| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 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 argparse |
| import contextlib |
| import copy |
| import io |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| import unittest |
| from pathlib import Path |
| from unittest import mock |
| |
| import fuchsia |
| import remote_action |
| import cl_utils |
| import output_leak_scanner |
| import remotetool |
| |
| from typing import Any, Dict, Sequence, Tuple |
| |
| _HAVE_XATTR = hasattr(os, "setxattr") |
| |
| |
| class ImmediateExit(Exception): |
| """For mocking functions that do not return.""" |
| |
| pass |
| |
| |
| def _write_file_contents(path: Path, contents: str): |
| with open(path, "w") as f: |
| f.write(contents) |
| |
| |
| def _read_file_contents(path: Path) -> str: |
| with open(path, "r") as f: |
| return f.read() |
| |
| |
| 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}") |
| |
| |
| def _fake_download_output( |
| packed_args: Tuple[ |
| remote_action.DownloadStubInfo, |
| remotetool.RemoteTool, |
| Path, |
| bool, |
| ] |
| ) -> Tuple[Path, cl_utils.SubprocessResult]: |
| # For mocking remote_action._download_output_for_mp. |
| # defined because multiprocessing cannot serialize mocks |
| stub_info, downloader, working_dir_abs, verbose = packed_args |
| # Don't actually try to download. |
| return (stub_info.path, cl_utils.SubprocessResult(0)) |
| |
| |
| def _fake_download_output_fail( |
| packed_args: Tuple[ |
| remote_action.DownloadStubInfo, |
| remotetool.RemoteTool, |
| Path, |
| bool, |
| ] |
| ) -> Tuple[Path, cl_utils.SubprocessResult]: |
| # For mocking remote_action._download_output_for_mp. |
| # defined because multiprocessing cannot serialize mocks |
| stub_info, downloader, working_dir_abs, verbose = packed_args |
| # Don't actually try to download. |
| return (stub_info.path, cl_utils.SubprocessResult(1)) |
| |
| |
| def _fake_download_input( |
| packed_args: Tuple[ |
| Path, |
| remotetool.RemoteTool, |
| Path, |
| bool, |
| ] |
| ) -> Tuple[Path, cl_utils.SubprocessResult]: |
| # For mocking remote_action._download_input_for_mp. |
| # defined because multiprocessing cannot serialize mocks |
| stub_path, downloader, working_dir_abs, verbose = packed_args |
| # Don't actually try to download. |
| return (stub_path, cl_utils.SubprocessResult(0)) |
| |
| |
| def _fake_download_input_fail( |
| packed_args: Tuple[ |
| Path, |
| remotetool.RemoteTool, |
| Path, |
| bool, |
| ] |
| ) -> Tuple[Path, cl_utils.SubprocessResult]: |
| # For mocking remote_action._download_input_for_mp. |
| # defined because multiprocessing cannot serialize mocks |
| stub_path, downloader, working_dir_abs, verbose = packed_args |
| # Don't actually try to download. |
| return (stub_path, cl_utils.SubprocessResult(1)) |
| |
| |
| class PathToDownloadStubTests(unittest.TestCase): |
| def test_is_stub(self): |
| path = Path("obj/stubby.stub") |
| fake_stub_info = remote_action.DownloadStubInfo( |
| path=path, |
| type="file", |
| blob_digest="0a97a6sbed/7711", |
| action_digest="bed977abaac/32", |
| build_id="random-id0348718", |
| ) |
| with mock.patch.object( |
| remote_action, "is_download_stub_file", return_value=True |
| ) as mock_check_stub: |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, |
| "read_from_file", |
| return_value=fake_stub_info, |
| ) as mock_read_stub: |
| stub = remote_action.path_to_download_stub(path) |
| self.assertEqual(stub, fake_stub_info) |
| mock_check_stub.assert_called_once_with(path) |
| mock_read_stub.assert_called_once_with(path) |
| |
| def test_not_stub(self): |
| path = Path("not/stubby.o") |
| with mock.patch.object( |
| remote_action, "is_download_stub_file", return_value=False |
| ) as mock_check_stub: |
| stub = remote_action.path_to_download_stub(path) |
| |
| self.assertIsNone(stub) |
| mock_check_stub.assert_called_once_with(path) |
| |
| |
| class DownloadFromStubPathTests(unittest.TestCase): |
| def test_stub_does_not_exist_ignored(self): |
| with tempfile.TemporaryDirectory() as td: |
| stub_path = Path(td) / "stub-not-exist" |
| with mock.patch.object( |
| Path, "exists", return_value=False |
| ) as mock_exists: |
| subprocess_result = remote_action.download_from_stub_path( |
| stub_path, |
| downloader=None, # not needed |
| working_dir_abs=Path(td), |
| ) |
| self.assertEqual(subprocess_result.returncode, 0) |
| |
| |
| class UndownloadTests(unittest.TestCase): |
| def test_undownload_non_stub_ignored(self): |
| path = Path("foo/barf.baz") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / path.parent).mkdir(parents=True) |
| (tdp / path).write_text("bye\n") |
| # path points to a non-stub |
| self.assertFalse(remote_action.is_download_stub_file(tdp / path)) |
| remote_action.undownload(tdp / path) |
| # nothing changes |
| self.assertFalse(remote_action.is_download_stub_file(tdp / path)) |
| |
| def test_undownload_restored(self): |
| path = Path("foo/barf.baz") |
| stub = remote_action.DownloadStubInfo( |
| path=path, |
| type="file", |
| blob_digest="82828abf872/453", |
| action_digest="2332df093d1/98", |
| build_id="random-id777", |
| ) |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| stub.create(tdp) |
| # Pretend to download first. |
| download_status = 0 |
| |
| def fake_download_file( |
| downloader_self, path: Path, digest: str, **kwargs |
| ): |
| path.write_text("greetings\n") |
| return cl_utils.SubprocessResult(download_status) |
| |
| with mock.patch.object( |
| remotetool.RemoteTool, "download_blob", new=fake_download_file |
| ) as mock_download: |
| status = stub.download( |
| downloader=_FAKE_DOWNLOADER, working_dir_abs=tdp |
| ) |
| |
| # path points to a non-stub |
| self.assertFalse(remote_action.is_download_stub_file(tdp / path)) |
| |
| remote_action.undownload(tdp / path) |
| # now path points to a restored stub |
| self.assertTrue(remote_action.is_download_stub_file(tdp / path)) |
| |
| |
| class DownloadOutputStubInfosBatchTests(unittest.TestCase): |
| def test_empty_list(self): |
| statuses = remote_action.download_output_stub_infos_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_infos=[], |
| working_dir_abs=Path("."), |
| ) |
| self.assertEqual(statuses, {}) |
| |
| def test_one_download_stub_downloaded_success(self): |
| path = Path("foo/bar.o") |
| fake_stub_info = remote_action.DownloadStubInfo( |
| path=path, |
| type="file", |
| blob_digest="1112313123/912", |
| action_digest="a7a77ed7f98/332", |
| build_id="random-id987198129", |
| ) |
| with mock.patch.object( |
| remote_action, "_download_output_for_mp", new=_fake_download_output |
| ) as mock_download: # success |
| statuses = remote_action.download_output_stub_infos_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_infos=[fake_stub_info], |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path].returncode, 0) |
| |
| def test_one_download_stub_downloaded_failure(self): |
| path = Path("foo/bar.o") |
| fake_stub_info = remote_action.DownloadStubInfo( |
| path=path, |
| type="file", |
| blob_digest="1112313123/912", |
| action_digest="a7a77ed7f98/332", |
| build_id="random-id987198129", |
| ) |
| with mock.patch.object( |
| remote_action, |
| "_download_output_for_mp", |
| new=_fake_download_output_fail, |
| ) as mock_download: |
| statuses = remote_action.download_output_stub_infos_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_infos=[fake_stub_info], |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path].returncode, 1) |
| |
| def test_multiple_download_stub_downloaded_success(self): |
| path1 = Path("foo/bar.o") |
| path2 = Path("baz/quux.o") |
| fake_stub_infos = [ |
| remote_action.DownloadStubInfo( |
| path=path1, |
| type="file", |
| blob_digest="767676767a767/912", |
| action_digest="a7a77ed7f98/332", |
| build_id="random-id0010129", |
| ), |
| remote_action.DownloadStubInfo( |
| path=path2, |
| type="file", |
| blob_digest="3e3e3e3e3e/712", |
| action_digest="1122777eecca/32", |
| build_id="random-id0012397", |
| ), |
| ] |
| with mock.patch.object( |
| remote_action, "_download_output_for_mp", new=_fake_download_output |
| ) as mock_download: # success |
| statuses = remote_action.download_output_stub_infos_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_infos=fake_stub_infos, |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path1].returncode, 0) |
| self.assertEqual(statuses[path2].returncode, 0) |
| |
| |
| class DownloadInputStubPathsBatchTests(unittest.TestCase): |
| def test_empty_list(self): |
| statuses = remote_action.download_input_stub_paths_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_paths=[], |
| working_dir_abs=Path("."), |
| ) |
| self.assertEqual(statuses, {}) |
| |
| def test_one_download_path_downloaded_success(self): |
| path = Path("foo/bar.o") |
| with mock.patch.object( |
| remote_action, "_download_input_for_mp", new=_fake_download_input |
| ) as mock_download: # success |
| statuses = remote_action.download_input_stub_paths_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_paths=[path], |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path].returncode, 0) |
| |
| def test_one_download_path_downloaded_failure(self): |
| path = Path("foo/bar.o") |
| with mock.patch.object( |
| remote_action, |
| "_download_input_for_mp", |
| new=_fake_download_input_fail, |
| ) as mock_download: |
| statuses = remote_action.download_input_stub_paths_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_paths=[path], |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path].returncode, 1) |
| |
| def test_multiple_download_stub_downloaded_success(self): |
| path1 = Path("foo/bar.o") |
| path2 = Path("baz/quux.o") |
| with mock.patch.object( |
| remote_action, "_download_input_for_mp", new=_fake_download_input |
| ) as mock_download: # success |
| statuses = remote_action.download_input_stub_paths_batch( |
| downloader=_FAKE_DOWNLOADER, |
| stub_paths=[path1, path2], |
| working_dir_abs=Path("."), |
| ) |
| |
| self.assertEqual(statuses[path1].returncode, 0) |
| self.assertEqual(statuses[path2].returncode, 0) |
| |
| |
| class FakeReproxyLogEntry(remote_action.ReproxyLogEntry): |
| """Mimic a ReproxyLogEntry by setting properties without parsing.""" |
| |
| def __init__(self, **kwargs): |
| # intentionally does not call super().__init__(), but instead |
| # sets property attributes. |
| for k, v in kwargs.items(): |
| setattr(self, "_" + k, v) |
| |
| @property |
| def execution_id(self) -> str: |
| return self._execution_id |
| |
| @property |
| def action_digest(self) -> str: |
| return self._action_digest |
| |
| @property |
| def output_file_digests(self) -> Dict[Path, str]: |
| return self._output_file_digests |
| |
| @property |
| def output_directory_digests(self) -> Dict[Path, str]: |
| return self._output_directory_digests |
| |
| @property |
| def completion_status(self) -> str: |
| return self._completion_status |
| |
| |
| class FileMatchTests(unittest.TestCase): |
| def test_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| f1path = Path(td, "left.txt") |
| f2path = Path(td, "right.txt") |
| _write_file_contents(f1path, "a\n") |
| _write_file_contents(f2path, "a\n") |
| self.assertTrue(remote_action._files_match(f1path, f2path)) |
| self.assertTrue(remote_action._files_match(f2path, f1path)) |
| |
| def test_not_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| f1path = Path(td, "left.txt") |
| f2path = Path(td, "right.txt") |
| _write_file_contents(f1path, "a\n") |
| _write_file_contents(f2path, "b\n") |
| self.assertFalse(remote_action._files_match(f1path, f2path)) |
| self.assertFalse(remote_action._files_match(f2path, f1path)) |
| |
| |
| class DetailDiffTests(unittest.TestCase): |
| def test_called(self): |
| with mock.patch.object( |
| cl_utils, |
| "subprocess_call", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_call: |
| self.assertEqual( |
| remote_action._detail_diff( |
| Path("file1.txt"), Path("file2.txt") |
| ).returncode, |
| 0, |
| ) |
| mock_call.assert_called_once() |
| first_call = mock_call.call_args_list[0] |
| args, unused_kwargs = first_call |
| command = args[0] # list |
| self.assertTrue( |
| command[0].endswith(str(remote_action._DETAIL_DIFF_SCRIPT)) |
| ) |
| |
| def test_filtered(self): |
| def _filter_for_compare( |
| file1: Path, filtered1: Path, file2: Path, filtered2: Path |
| ) -> bool: |
| # Pretend we wrote filtered views to filtered1 and filtered2. |
| return True |
| |
| with mock.patch.object( |
| cl_utils, |
| "subprocess_call", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_call: |
| self.assertEqual( |
| remote_action._detail_diff_filtered( |
| Path("file1.txt"), |
| Path("file2.txt"), |
| maybe_transform_pair=_filter_for_compare, |
| ).returncode, |
| 0, |
| ) |
| mock_call.assert_called_once() |
| first_call = mock_call.call_args_list[0] |
| args, unused_kwargs = first_call |
| command = args[0] # list |
| self.assertTrue( |
| command[0].endswith(str(remote_action._DETAIL_DIFF_SCRIPT)) |
| ) |
| self.assertEqual(command[1], "file1.txt.filtered") |
| self.assertEqual(command[2], "file2.txt.filtered") |
| |
| |
| class TextDiffTests(unittest.TestCase): |
| def test_called(self): |
| result = cl_utils.SubprocessResult(0) |
| with mock.patch.object( |
| cl_utils, "subprocess_call", return_value=result |
| ) as mock_call: |
| self.assertEqual( |
| remote_action._text_diff("file1.txt", "file2.txt"), result |
| ) |
| mock_call.assert_called_once() |
| first_call = mock_call.call_args_list[0] |
| args, unused_kwargs = first_call |
| command = args[0] # list |
| self.assertEqual(command[0], "diff") |
| self.assertEqual(command[-2:], ["file1.txt", "file2.txt"]) |
| |
| def test_matches(self): # no mocking |
| with tempfile.TemporaryDirectory() as td: |
| f1 = Path(td) / "left.txt" |
| f2 = Path(td) / "right.txt" |
| contents = "The quick brown fox\njumped over the lazy\ndogs.\n" |
| _write_file_contents(f1, contents) |
| _write_file_contents(f2, contents) |
| result = remote_action._text_diff(f1, f2) |
| self.assertEqual(result.returncode, 0) |
| self.assertEqual(result.stdout, []) |
| |
| def test_not_matches(self): # no mocking |
| with tempfile.TemporaryDirectory() as td: |
| f1 = Path(td) / "left.txt" |
| f2 = Path(td) / "right.txt" |
| contents = "The quick brown fox\njumped over the lazy\ndogs.\n" |
| _write_file_contents(f1, contents) |
| _write_file_contents(f2, contents.replace("m", "M")) |
| result = remote_action._text_diff(f1, f2) |
| self.assertEqual(result.returncode, 1) |
| self.assertNotEqual(result.stdout, []) |
| |
| |
| class FilesUnderDirTests(unittest.TestCase): |
| def test_walk(self): |
| with tempfile.TemporaryDirectory() as td: |
| f1path = Path(td) / "left.txt" |
| subdir = Path(td) / "sub" |
| os.mkdir(subdir) |
| f2path = subdir / "right.txt" |
| _write_file_contents(f1path, "\n") |
| _write_file_contents(f2path, "\n") |
| self.assertEqual( |
| set(remote_action._files_under_dir(td)), |
| _paths({"left.txt", "sub/right.txt"}), |
| ) |
| |
| |
| class CommonFilesUnderDirsTests(unittest.TestCase): |
| def test_none_in_common(self): |
| with mock.patch.object( |
| remote_action, |
| "_files_under_dir", |
| side_effect=[iter(["a", "b", "c"]), iter(["d", "e", "f"])], |
| ) as mock_lsr: |
| self.assertEqual( |
| remote_action._common_files_under_dirs("foo-dir", "bar-dir"), |
| set(), |
| ) |
| |
| def test_some_in_common(self): |
| with mock.patch.object( |
| remote_action, |
| "_files_under_dir", |
| side_effect=[ |
| iter(_paths(["a", "b/x", "c"])), |
| iter(_paths(["d", "c", "b/x"])), |
| ], |
| ) as mock_lsr: |
| self.assertEqual( |
| remote_action._common_files_under_dirs( |
| Path("foo-dir"), Path("bar-dir") |
| ), |
| _paths({"b/x", "c"}), |
| ) |
| |
| |
| class ExpandCommonFilesBetweenDirs(unittest.TestCase): |
| def test_common(self): |
| # Normally returns a set, but mock-return a list for deterministic |
| # ordering. |
| with mock.patch.object( |
| remote_action, |
| "_common_files_under_dirs", |
| return_value=_paths(["y/z", "x"]), |
| ) as mock_ls: |
| self.assertEqual( |
| list( |
| remote_action._expand_common_files_between_dirs( |
| [_paths(("c", "d")), _paths(("a", "b"))] |
| ) |
| ), |
| [ |
| _paths(("c/x", "d/x")), |
| _paths(("c/y/z", "d/y/z")), |
| _paths(("a/x", "b/x")), |
| _paths(("a/y/z", "b/y/z")), |
| ], |
| ) |
| |
| |
| class FileLinesMatchingTests(unittest.TestCase): |
| def test_empty(self): |
| with mock.patch( |
| "builtins.open", mock.mock_open(read_data="") |
| ) as mock_file: |
| self.assertEqual( |
| list( |
| remote_action._file_lines_matching("log.txt", "never-match") |
| ), |
| [], |
| ) |
| |
| def test_matches(self): |
| with mock.patch( |
| "builtins.open", mock.mock_open(read_data="ab\nbc\ncd\n") |
| ) as mock_file: |
| self.assertEqual( |
| list(remote_action._file_lines_matching("file.txt", "c")), |
| ["bc\n", "cd\n"], |
| ) |
| |
| |
| class TransformFileByLines(unittest.TestCase): |
| def test_no_change(self): |
| with tempfile.TemporaryDirectory() as td: |
| f1 = Path(td) / "in.txt" |
| f2 = Path(td) / "out.txt" |
| _write_file_contents( |
| f1, "aa\n\n\nbb\ncc dd\n\ne f \n gh ij\n k l \n" |
| ) |
| remote_action._transform_file_by_lines(f1, f2, lambda x: x) |
| s1 = _read_file_contents(f1) |
| s2 = _read_file_contents(f2) |
| self.assertEqual(s1, s2) |
| |
| |
| class ReclientCanonicalWorkingDirTests(unittest.TestCase): |
| def test_empty(self): |
| self.assertEqual( |
| remote_action.reclient_canonical_working_dir(Path("")), Path("") |
| ) |
| |
| def test_one_level(self): |
| self.assertEqual( |
| remote_action.reclient_canonical_working_dir(Path("build-here")), |
| Path("set_by_reclient"), |
| ) |
| |
| def test_two_levels(self): |
| self.assertEqual( |
| remote_action.reclient_canonical_working_dir(Path("build/there")), |
| Path("set_by_reclient/a"), |
| ) |
| |
| def test_three_levels(self): |
| self.assertEqual( |
| remote_action.reclient_canonical_working_dir( |
| Path("build/inside/there") |
| ), |
| Path("set_by_reclient/a/a"), |
| ) |
| |
| |
| class RewriteDepfileTests(unittest.TestCase): |
| def test_depfile_in_place(self): |
| with tempfile.TemporaryDirectory() as td: |
| depfile = Path(td) / "dep.d" |
| |
| wd = Path("/home/base/out/inside/here") |
| _write_file_contents( |
| depfile, f"obj/foo.o: {wd}/foo/bar.h {wd}/baz/quux.h\n" |
| ) |
| remote_action.rewrite_depfile( |
| depfile, |
| transform=lambda x: remote_action._remove_prefix(x, f"{wd}/"), |
| ) # write in-place |
| self.assertEqual( |
| _read_file_contents(depfile), |
| "obj/foo.o: foo/bar.h baz/quux.h\n", |
| ) |
| |
| def test_depfile_new_file(self): |
| with tempfile.TemporaryDirectory() as td: |
| depfile = Path(td) / "dep.d" |
| output = depfile.with_suffix(".new") |
| |
| wd = Path("/all/your/base") |
| _write_file_contents( |
| depfile, f"obj/foo.o: {wd}/foo/bar.h {wd}/baz/quux.h\n" |
| ) |
| remote_action.rewrite_depfile( |
| depfile, |
| transform=lambda x: remote_action._remove_prefix(x, f"{wd}/"), |
| output=output, |
| ) # write new file |
| self.assertEqual( |
| _read_file_contents(output), |
| "obj/foo.o: foo/bar.h baz/quux.h\n", |
| ) |
| |
| |
| class ResolvedShlibsFromLddTests(unittest.TestCase): |
| def test_sample(self): |
| ldd_output = """ |
| linux-vdso.so.1 (0x00007ffd653b2000) |
| librustc_driver-897e90da9cc472c4.so => /usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/librustc_driver-897e90da9cc472c4.so (0x00007f6fdf600000) |
| libstd-374958b5d3497a8f.so => /usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/libstd-374958b5d3497a8f.so (0x00007f6fdf45c000) |
| libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6fe2cc6000) |
| librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f6fe2cc1000) |
| libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6fe2cba000) |
| libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6fdf27b000) |
| libLLVM-15-rust-1.70.0-nightly.so => /usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/../lib/libLLVM-15-rust-1.70.0-nightly.so (0x00007f6fdb000000) |
| libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6fe2921000) |
| /lib64/ld-linux-x86-64.so.2 (0x00007f6fe2ce6000) |
| """ |
| self.assertEqual( |
| list( |
| remote_action.resolved_shlibs_from_ldd(ldd_output.splitlines()) |
| ), |
| _paths( |
| [ |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/librustc_driver-897e90da9cc472c4.so", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/libstd-374958b5d3497a8f.so", |
| "/lib/x86_64-linux-gnu/libdl.so.2", |
| "/lib/x86_64-linux-gnu/librt.so.1", |
| "/lib/x86_64-linux-gnu/libpthread.so.0", |
| "/lib/x86_64-linux-gnu/libc.so.6", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/../lib/libLLVM-15-rust-1.70.0-nightly.so", |
| "/lib/x86_64-linux-gnu/libm.so.6", |
| ] |
| ), |
| ) |
| |
| |
| class HostToolNonsystemShlibsTests(unittest.TestCase): |
| def test_sample(self): |
| unfiltered_shlibs = _paths( |
| [ |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/librustc_driver-897e90da9cc472c4.so", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/libstd-374958b5d3497a8f.so", |
| "/lib/x86_64-linux-gnu/libdl.so.2", |
| "/lib/x86_64-linux-gnu/librt.so.1", |
| "/lib/x86_64-linux-gnu/libpthread.so.0", |
| "/lib/x86_64-linux-gnu/libc.so.6", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/../lib/libLLVM-15-rust-1.70.0-nightly.so", |
| "/lib/x86_64-linux-gnu/libm.so.6", |
| "/usr/lib/something_else.so", |
| ] |
| ) |
| with mock.patch.object( |
| remote_action, "host_tool_shlibs", return_value=unfiltered_shlibs |
| ) as mock_host_tool_shlibs: |
| self.assertEqual( |
| list( |
| remote_action.host_tool_nonsystem_shlibs("../path/to/rustc") |
| ), |
| _paths( |
| [ |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/librustc_driver-897e90da9cc472c4.so", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/libstd-374958b5d3497a8f.so", |
| "/usr/home/janedoe/my_project/tools/rust/linux-x64/bin/../lib/../lib/libLLVM-15-rust-1.70.0-nightly.so", |
| ] |
| ), |
| ) |
| mock_host_tool_shlibs.assert_called_once() |
| |
| |
| class RewrapperArgParserTests(unittest.TestCase): |
| @property |
| def _parser(self): |
| return remote_action._REWRAPPER_ARG_PARSER |
| |
| def test_default(self): |
| args, _ = self._parser.parse_known_args([]) |
| self.assertIsNone(args.exec_root) |
| self.assertIsNone(args.canonicalize_working_dir) |
| |
| def test_exec_root(self): |
| args, _ = self._parser.parse_known_args(["--exec_root=/foo/bar"]) |
| self.assertEqual(args.exec_root, "/foo/bar") |
| |
| def test_canonicalize_working_dir_true(self): |
| args, _ = self._parser.parse_known_args( |
| ["--canonicalize_working_dir=true"] |
| ) |
| self.assertTrue(args.canonicalize_working_dir) |
| |
| def test_canonicalize_working_dir_false(self): |
| args, _ = self._parser.parse_known_args( |
| ["--canonicalize_working_dir=false"] |
| ) |
| self.assertFalse(args.canonicalize_working_dir) |
| |
| def test_help_unwanted(self): |
| for opt in ("-h", "--help"): |
| with mock.patch.object(sys, "exit") as mock_exit: |
| self._parser.parse_known_args([opt]) |
| mock_exit.assert_not_called() |
| |
| |
| class RemoteActionMainParserTests(unittest.TestCase): |
| @property |
| def default_cfg(self): |
| return Path("default.cfg") |
| |
| @property |
| def default_bindir(self): |
| return Path("/opt/reclient/bin") |
| |
| def _make_main_parser(self) -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser() |
| remote_action.inherit_main_arg_parser_flags( |
| parser, |
| default_cfg=self.default_cfg, |
| default_bindir=self.default_bindir, |
| ) |
| return parser |
| |
| def test_defaults(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--", "echo", "hello"]) |
| self.assertEqual(main_args.cfg, self.default_cfg) |
| self.assertEqual(main_args.bindir, self.default_bindir) |
| self.assertFalse(main_args.dry_run) |
| self.assertFalse(main_args.verbose) |
| self.assertEqual(main_args.label, "") |
| self.assertEqual(main_args.remote_log, "") |
| self.assertFalse(main_args.save_temps) |
| self.assertIsNone(main_args.fsatrace_path) |
| self.assertFalse(main_args.compare) |
| self.assertFalse(main_args.diagnose_nonzero) |
| self.assertEqual(main_args.command, ["echo", "hello"]) |
| self.assertIsNone(main_args.remote_debug_command) |
| |
| def test_cfg(self): |
| p = self._make_main_parser() |
| cfg = Path("other.cfg") |
| main_args, other = p.parse_known_args([f"--cfg={cfg}", "--", "echo"]) |
| self.assertEqual(main_args.cfg, cfg) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertEqual(action.local_only_command, ["echo"]) |
| self.assertEqual(action.options, ["--cfg", str(cfg)]) |
| |
| def test_bindir(self): |
| p = self._make_main_parser() |
| bindir = Path("/usr/local/bin") |
| main_args, other = p.parse_known_args( |
| ["--bindir", str(bindir), "--", "echo"] |
| ) |
| self.assertEqual(main_args.bindir, bindir) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertEqual(action.local_only_command, ["echo"]) |
| |
| def test_local_command_with_env(self): |
| local_command = ["FOO=BAR", "echo"] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--"] + local_command) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertEqual( |
| action.local_only_command, [cl_utils._ENV] + local_command |
| ) |
| self.assertEqual( |
| action.remote_only_command, [cl_utils._ENV] + local_command |
| ) |
| |
| def test_verbose(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--verbose", "--", "echo"]) |
| self.assertTrue(main_args.verbose) |
| |
| def test_dry_run(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--dry-run", "--", "echo"]) |
| self.assertTrue(main_args.dry_run) |
| action = remote_action.remote_action_from_args(main_args) |
| with mock.patch.object(remote_action.RemoteAction, "run") as mock_run: |
| exit_code = action.run_with_main_args(main_args) |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_not_called() |
| |
| @mock.patch.object(fuchsia, "REPROXY_WRAP", "/path/to/reproxy-wrap.sh") |
| def test_auto_reproxy(self): |
| # --auto-reproxy is now obsolete, and will be removed in the future |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--auto-reproxy", "--", "echo"]) |
| self.assertTrue(main_args.auto_reproxy) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertEqual(action.local_only_command, ["echo"]) |
| rewrapper_prefix, _, remote_command = cl_utils.partition_sequence( |
| action.launch_command, "--" |
| ) |
| self.assertEqual(Path(rewrapper_prefix[0]).name, "rewrapper") |
| self.assertEqual(remote_command, ["echo"]) |
| |
| def test_save_temps(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--save-temps", "--", "echo"]) |
| self.assertTrue(main_args.save_temps) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertEqual(action.local_only_command, ["echo"]) |
| self.assertTrue(action.save_temps) |
| |
| def test_label(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--label=//build/this:that", "--", "echo"] |
| ) |
| self.assertEqual(main_args.label, "//build/this:that") |
| |
| def test_diagnose_nonzero(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--diagnose-nonzero", "--", "echo"] |
| ) |
| self.assertTrue(main_args.diagnose_nonzero) |
| action = remote_action.remote_action_from_args(main_args) |
| self.assertTrue(action.diagnose_nonzero) |
| |
| def test_input_list_paths(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| input1 = Path("hello.txt") |
| input2 = Path("goodbye.txt") |
| with tempfile.TemporaryDirectory() as td: |
| rspfile1 = Path(td) / "inputs.rsp" |
| _write_file_contents(rspfile1, f"{input1}\n") |
| rspfile2 = Path(td) / "more-inputs.rsp" |
| _write_file_contents(rspfile2, f"{input2}\n") |
| |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| _strs( |
| [ |
| f"--input_list_paths={rspfile1},{rspfile2}", |
| "--", |
| "cat", |
| input1, |
| input2, |
| ] |
| ) |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| set(action.inputs_relative_to_project_root), |
| { |
| build_dir / input1, |
| build_dir / input2, |
| }, # relative to exec_root |
| ) |
| |
| def test_remote_debug_command(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| input = Path("hello.txt") |
| debug = "ls -l -R .." |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| _strs([f"--remote-debug-command={debug}", "--", "cat", input]) |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| inputs=[input], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual(action.remote_debug_command, debug.split()) |
| self.assertEqual( |
| {build_dir / input}, set(action.inputs_relative_to_project_root) |
| ) |
| with mock.patch.object(cl_utils, "subprocess_call") as mock_remote: |
| self.assertEqual(action.run(), 1) |
| mock_remote.assert_called_once() |
| arg, kwargs = mock_remote.call_args_list[0] |
| full_command = arg[0] |
| rewrapper_prefix, sep, remote_command = cl_utils.partition_sequence( |
| full_command, "--" |
| ) |
| self.assertEqual(remote_command, debug.split()) |
| |
| def test_remote_log_named(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--log", "bar.remote-log", "--", "echo"] |
| ) |
| self.assertEqual(main_args.remote_log, "bar.remote-log") |
| |
| def test_remote_log_unnamed(self): |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--log", "--", "echo"]) |
| self.assertEqual(main_args.remote_log, "<AUTO>") |
| |
| def test_remote_log_from_main_args_auto_named(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args(["--log", "--"] + command) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| [remote_action._REMOTE_LOG_SCRIPT], |
| action.inputs_relative_to_project_root, |
| ) |
| self.assertEqual( |
| {build_dir / output, build_dir / (str(output) + ".remote-log")}, |
| set(action.output_files_relative_to_project_root), |
| ) |
| # Ignore the rewrapper portion of the command |
| full_command = action.launch_command |
| command_slices = list( |
| cl_utils.split_into_subsequences(full_command, "--") |
| ) |
| prefix, log_wrapper, main_command = command_slices |
| # Confirm that the remote command is wrapped with the logger script. |
| self.assertEqual( |
| log_wrapper, |
| _strs( |
| [ |
| Path("..", remote_action._REMOTE_LOG_SCRIPT), |
| "--log", |
| str(output) + ".remote-log", |
| ] |
| ), |
| ) |
| self.assertEqual(main_command, command) |
| |
| def test_remote_log_from_main_args_explicitly_named(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| log_base = "debug" |
| p = self._make_main_parser() |
| command = ["touch", str(output)] |
| main_args, other = p.parse_known_args( |
| ["--log", log_base, "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| [remote_action._REMOTE_LOG_SCRIPT], |
| action.inputs_relative_to_project_root, |
| ) |
| self.assertEqual( |
| { |
| build_dir / output, |
| build_dir / (log_base + ".remote-log"), |
| }, |
| set(action.output_files_relative_to_project_root), |
| ) |
| # Ignore the rewrapper portion of the command |
| command_slices = list( |
| cl_utils.split_into_subsequences(action.launch_command, "--") |
| ) |
| prefix, log_wrapper, main_command = command_slices |
| # Confirm that the remote command is wrapped with the logger script. |
| self.assertEqual( |
| log_wrapper, |
| _strs( |
| [ |
| Path("..", remote_action._REMOTE_LOG_SCRIPT), |
| "--log", |
| log_base + ".remote-log", |
| ] |
| ), |
| ) |
| self.assertEqual(main_command, command) |
| |
| def test_remote_fsatrace_path_default(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| fake_fsatrace = fuchsia.FSATRACE_PATH |
| fake_fsatrace_rel = Path(f"../{fake_fsatrace}") |
| p = self._make_main_parser() |
| command = ["touch", str(output)] |
| # Pass "" to use the default fuchsia.FSATRACE_PATH |
| main_args, other = p.parse_known_args( |
| ["--fsatrace-path=", "--"] + command |
| ) |
| self.assertEqual(main_args.fsatrace_path, Path("")) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| {fake_fsatrace, fake_fsatrace.with_suffix(".so")}, |
| set(action.inputs_relative_to_project_root), |
| ) |
| self.assertEqual( |
| { |
| build_dir / output, # relative to exec_root |
| build_dir / (str(output) + ".remote-fsatrace"), |
| }, |
| set(action.output_files_relative_to_project_root), |
| ) |
| # Ignore the rewrapper portion of the command |
| cmd_slices = cl_utils.split_into_subsequences( |
| action.launch_command, "--" |
| ) |
| rewrapper_prefix, fsatrace_prefix, remote_command = cmd_slices |
| # Confirm that the remote command is wrapped with fsatrace |
| self.assertIn(str(fake_fsatrace_rel), fsatrace_prefix) |
| self.assertEqual( |
| fsatrace_prefix + ["--"], |
| action._fsatrace_command_prefix( |
| Path(str(output) + ".remote-fsatrace") |
| ), |
| ) |
| self.assertEqual(remote_command, command) |
| |
| def test_remote_fsatrace_from_main_args(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| fake_fsatrace = Path("tools/debug/fsatrace") |
| fake_fsatrace_rel = Path(f"../{fake_fsatrace}") |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| _strs(["--fsatrace-path", fake_fsatrace_rel, "--", "touch", output]) |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| {fake_fsatrace, fake_fsatrace.with_suffix(".so")}, |
| set(action.inputs_relative_to_project_root), |
| ) |
| self.assertEqual( |
| { |
| build_dir / output, |
| build_dir / (str(output) + ".remote-fsatrace"), |
| }, |
| set(action.output_files_relative_to_project_root), |
| ) |
| # Ignore the rewrapper portion of the command |
| full_command = action.launch_command |
| command_slices = list( |
| cl_utils.split_into_subsequences(action.launch_command, "--") |
| ) |
| prefix, trace_wrapper, main_command = command_slices |
| # Confirm that the remote command is wrapped with fsatrace |
| self.assertIn(str(fake_fsatrace_rel), trace_wrapper) |
| self.assertEqual( |
| trace_wrapper + ["--"], |
| action._fsatrace_command_prefix( |
| Path(str(output) + ".remote-fsatrace") |
| ), |
| ) |
| self.assertEqual(main_command, ["touch", str(output)]) |
| |
| def test_remote_log_and_fsatrace_from_main_args(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| command = ["touch", str(output)] |
| fake_fsatrace = Path("tools/debug/fsatrace") |
| fake_fsatrace_rel = Path("..", fake_fsatrace) |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| _strs( |
| ["--fsatrace-path", fake_fsatrace_rel, "--log", "--"] + command |
| ) |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| self.assertEqual( |
| { |
| remote_action._REMOTE_LOG_SCRIPT, |
| fake_fsatrace, |
| fake_fsatrace.with_suffix(".so"), |
| }, |
| set(action.inputs_relative_to_project_root), |
| ) |
| self.assertEqual( |
| { |
| build_dir / output, |
| build_dir / (str(output) + ".remote-log"), |
| build_dir / (str(output) + ".remote-fsatrace"), |
| }, |
| set(action.output_files_relative_to_project_root), |
| ) |
| # Ignore the rewrapper portion of the command |
| command_slices = list( |
| cl_utils.split_into_subsequences(action.launch_command, "--") |
| ) |
| ( |
| rewrapper_prefix, |
| log_wrapper, |
| trace_wrapper, |
| main_command, |
| ) = command_slices |
| # Confirm that the outer wrapper is for logging |
| self.assertEqual( |
| log_wrapper, |
| _strs( |
| [ |
| Path("..", remote_action._REMOTE_LOG_SCRIPT), |
| "--log", |
| str(output) + ".remote-log", |
| ] |
| ), |
| ) |
| # Confirm that the inner wrapper is for fsatrace |
| self.assertEqual( |
| trace_wrapper + ["--"], |
| action._fsatrace_command_prefix( |
| Path(str(output) + ".remote-fsatrace") |
| ), |
| ) |
| self.assertEqual(main_command, command) |
| |
| def test_local_only_no_compare(self): |
| # --compare does nothing with --local |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--compare", "--local", "--"] + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.remote_disable) |
| self.assertTrue(action.compare_with_local) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, "run", return_value=0 |
| ) as mock_run: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_compare_against_local" |
| ) as mock_compare: |
| exit_code = action.run_with_main_args(main_args) |
| |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called_once() |
| mock_compare.assert_not_called() |
| |
| def test_compare_forces_remote(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--compare", "--exec_strategy=local", "--"] + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertFalse(action.remote_disable) |
| self.assertTrue(action.compare_with_local) |
| self.assertEqual(action.exec_strategy, "remote") # forced |
| |
| def test_compare_fsatraces_acceptable_match(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build/out/here") |
| working_dir = exec_root / build_dir |
| p = self._make_main_parser() |
| action = remote_action.RemoteAction( |
| rewrapper=Path("/test-build/rewrapper"), |
| command=["sleep", "1h"], |
| options=["--canonicalize_working_dir=true"], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.canonicalize_working_dir) |
| local_trace_contents = f"""r|{exec_root}/src/input.c |
| w|{working_dir}/obj/input.o |
| """ |
| remote_root = remote_action._REMOTE_PROJECT_ROOT |
| remote_trace_contents = f"""r|{remote_root}/src/input.c |
| w|{remote_root}/set_by_reclient/a/a/obj/input.o |
| """ |
| self.assertNotEqual(local_trace_contents, remote_trace_contents) |
| with tempfile.TemporaryDirectory() as td: |
| local_trace = Path(td) / "local.trace" |
| remote_trace = Path(td) / "remote.trace" |
| _write_file_contents(local_trace, local_trace_contents) |
| _write_file_contents(remote_trace, remote_trace_contents) |
| diff_text = io.StringIO() |
| with contextlib.redirect_stdout(diff_text): |
| status = action._compare_fsatraces_select_logs( |
| local_trace=local_trace, |
| remote_trace=remote_trace, |
| ) |
| self.assertEqual(status.returncode, 0) # contents are equivalent |
| |
| def test_compare_fsatraces_with_difference(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build/out/here") |
| working_dir = exec_root / build_dir |
| p = self._make_main_parser() |
| action = remote_action.RemoteAction( |
| rewrapper=Path("/test-build/rewrapper"), |
| command=["sleep", "1h"], |
| options=["--canonicalize_working_dir=true"], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.canonicalize_working_dir) |
| local_trace_contents = f"""r|{exec_root}/src/input.c |
| w|{working_dir}/obj/input.o |
| """ |
| remote_root = remote_action._REMOTE_PROJECT_ROOT |
| remote_trace_contents = f"""r|{remote_root}/src/input.c |
| r|{remote_root}/includes/input.h |
| w|{remote_root}/set_by_reclient/a/a/obj/input.o |
| """ |
| self.assertNotEqual(local_trace_contents, remote_trace_contents) |
| with tempfile.TemporaryDirectory() as td: |
| local_trace = Path(td) / "local.trace" |
| remote_trace = Path(td) / "remote.trace" |
| _write_file_contents(local_trace, local_trace_contents) |
| _write_file_contents(remote_trace, remote_trace_contents) |
| diff_text = io.StringIO() |
| with contextlib.redirect_stdout(diff_text): |
| result = action._compare_fsatraces_select_logs( |
| local_trace=local_trace, |
| remote_trace=remote_trace, |
| ) |
| self.assertEqual(result.returncode, 1) # traces differ |
| |
| def test_local_remote_compare_no_diffs_from_main_args(self): |
| # Same as test_remote_fsatrace_from_main_args, but with --compare |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--compare", "--"] + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.compare_with_local) |
| |
| unnamed_mocks = [ |
| # we don't bother to check the call details of these mocks |
| mock.patch.object(Path, "rename"), |
| mock.patch.object(Path, "is_file", return_value=True), |
| # Pretend comparison finds no differences |
| mock.patch.object(remote_action, "_files_match", return_value=True), |
| ] |
| with contextlib.ExitStack() as stack: |
| for m in unnamed_mocks: |
| stack.enter_context(m) |
| |
| # both local and remote commands succeed |
| with mock.patch.object( |
| remote_action.RemoteAction, "_run_locally", return_value=0 |
| ) as mock_local_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_remote_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_compare_fsatraces" |
| ) as mock_compare_traces: |
| with mock.patch.object(os, "remove") as mock_cleanup: |
| exit_code = action.run_with_main_args(main_args) |
| |
| remote_command = action.launch_command |
| self.assertEqual(exit_code, 0) # remote success and compare success |
| mock_compare_traces.assert_not_called() |
| mock_local_launch.assert_called_once() |
| mock_remote_launch.assert_called_once() |
| self.assertEqual(remote_command[-2:], base_command) |
| mock_cleanup.assert_called_with(Path(str(output) + ".remote")) |
| |
| def test_local_remote_compare_found_diffs_from_main_args(self): |
| # Same as test_remote_fsatrace_from_main_args, but with --compare |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--compare", "--"] + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.compare_with_local) |
| |
| unnamed_mocks = [ |
| # we don't bother to check the call details of these mocks |
| mock.patch.object(Path, "rename"), |
| mock.patch.object(Path, "is_file", return_value=True), |
| # Pretend comparison finds differences |
| mock.patch.object( |
| remote_action, "_files_match", return_value=False |
| ), |
| mock.patch.object(remote_action, "_detail_diff"), |
| ] |
| with contextlib.ExitStack() as stack: |
| for m in unnamed_mocks: |
| stack.enter_context(m) |
| |
| # both local and remote commands succeed |
| with mock.patch.object( |
| remote_action.RemoteAction, "_run_locally", return_value=0 |
| ) as mock_local_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_remote_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_compare_fsatraces" |
| ) as mock_compare_traces: |
| exit_code = action.run_with_main_args(main_args) |
| |
| remote_command = action.launch_command |
| self.assertEqual(exit_code, 1) # remote success, but compare failure |
| mock_compare_traces.assert_not_called() |
| mock_local_launch.assert_called_once() |
| mock_remote_launch.assert_called_once() |
| self.assertEqual(remote_command[-2:], base_command) |
| |
| def test_local_remote_compare_found_diffs_exported_files(self): |
| # Checks that miscompared files are exported. |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| input = Path("../greet.in") |
| export_dir = Path("naughty/diffs") # relative to working dir |
| export_dir_abs = working_dir / export_dir |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--compare", f"--miscomparison-export-dir={export_dir}", "--"] |
| + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| inputs=[input], |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.compare_with_local) |
| self.assertEqual( |
| action.miscomparison_export_dir, working_dir / export_dir |
| ) |
| |
| unnamed_mocks = [ |
| # we don't bother to check the call details of these mocks |
| mock.patch.object(Path, "rename"), |
| mock.patch.object(Path, "is_file", return_value=True), |
| # Pretend comparison finds differences |
| mock.patch.object( |
| remote_action, "_files_match", return_value=False |
| ), |
| mock.patch.object(remote_action, "_detail_diff"), |
| ] |
| with contextlib.ExitStack() as stack: |
| for m in unnamed_mocks: |
| stack.enter_context(m) |
| |
| # both local and remote commands succeed |
| with mock.patch.object( |
| remote_action.RemoteAction, "_run_locally", return_value=0 |
| ) as mock_local_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_remote_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_compare_fsatraces" |
| ) as mock_compare_traces: |
| with mock.patch.object( |
| cl_utils, |
| "chdir_cm", |
| return_value=contextlib.nullcontext(), |
| ) as mock_chdir: |
| with mock.patch.object( |
| cl_utils, "copy_preserve_subpath" |
| ) as mock_export: |
| exit_code = action.run_with_main_args(main_args) |
| |
| remote_command = action.launch_command |
| self.assertEqual(exit_code, 1) # remote success, but compare failure |
| mock_compare_traces.assert_not_called() |
| mock_local_launch.assert_called_once() |
| mock_remote_launch.assert_called_once() |
| self.assertEqual(remote_command[-2:], base_command) |
| # Make sure we copied the differences to the export dir |
| mock_export.assert_has_calls( |
| [ |
| mock.call(build_dir / output, export_dir_abs), |
| mock.call( |
| build_dir / Path(str(output) + ".remote"), export_dir_abs |
| ), |
| mock.call(Path("greet.in"), export_dir_abs), |
| ], |
| any_order=True, |
| ) |
| mock_chdir.assert_called_with(exec_root) |
| |
| def test_local_remote_compare_with_fsatrace_from_main_args(self): |
| # Same as test_remote_fsatrace_from_main_args, but with --compare |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| output = Path("hello.txt") |
| fake_fsatrace = Path("tools/debug/fsatrace") |
| fake_fsatrace_rel = Path("..", fake_fsatrace) |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| _strs( |
| [ |
| "--compare", |
| "--fsatrace-path", |
| fake_fsatrace_rel, |
| "--", |
| "touch", |
| output, |
| ] |
| ) |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| |
| # not repeating the same asserts from |
| # test_remote_fsatrace_from_main_args: |
| |
| unnamed_mocks = [ |
| # we don't bother to check the call details of these mocks |
| mock.patch.object(Path, "rename"), |
| mock.patch.object(Path, "is_file", return_value=True), |
| # Pretend comparison finds differences |
| mock.patch.object( |
| remote_action, "_files_match", return_value=False |
| ), |
| mock.patch.object(remote_action, "_detail_diff"), |
| # in RemoteAction._compare_fsatraces: |
| mock.patch.object(remote_action, "_transform_file_by_lines"), |
| ] |
| with contextlib.ExitStack() as stack: |
| for m in unnamed_mocks: |
| stack.enter_context(m) |
| |
| # both local and remote commands succeed |
| with mock.patch.object( |
| remote_action.RemoteAction, "_run_locally", return_value=0 |
| ) as mock_local_launch: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_remote_launch: |
| with mock.patch.object( |
| remote_action, |
| "_text_diff", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_trace_diff: |
| exit_code = action.run_with_main_args(main_args) |
| |
| remote_command = action.launch_command |
| # make sure local command is also traced |
| local_command = list(action._generate_local_launch_command()) |
| self.assertEqual(exit_code, 1) # remote success, but compare failure |
| mock_remote_launch.assert_called_once() |
| mock_local_launch.assert_called_once() |
| self.assertIn(str(fake_fsatrace_rel), remote_command) |
| self.assertIn(str(fake_fsatrace_rel), local_command) |
| remote_trace = str(output) + ".remote-fsatrace" |
| local_trace = str(output) + ".local-fsatrace" |
| self.assertIn(remote_trace, remote_command) |
| self.assertIn(local_trace, local_command) |
| mock_trace_diff.assert_called_with( |
| Path(local_trace + ".norm"), Path(remote_trace + ".norm") |
| ) |
| |
| def test_local_check_determinism(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| exec_root_rel = cl_utils.relpath(exec_root, start=working_dir) |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| main_args, other = p.parse_known_args( |
| ["--check-determinism", "--local", "--"] + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.remote_disable) |
| |
| with mock.patch.object( |
| cl_utils, |
| "subprocess_call", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run_with_main_args(main_args) |
| |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| launch_command = args[0] |
| self.assertEqual(kwargs["cwd"], working_dir) |
| check_prefix, sep, main_command = cl_utils.partition_sequence( |
| launch_command, "--" |
| ) |
| self.assertEqual(check_prefix[0], sys.executable) |
| self.assertIn( |
| str(exec_root_rel / fuchsia._CHECK_DETERMINISM_SCRIPT), check_prefix |
| ) |
| self.assertIn("--check-repeatability", check_prefix) |
| _, _, output_list = cl_utils.partition_sequence( |
| check_prefix, "--outputs" |
| ) |
| self.assertEqual(output_list, [str(output)]) |
| self.assertEqual(main_command, base_command) |
| |
| def test_local_check_determinism_with_export(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| exec_root_rel = cl_utils.relpath(exec_root, start=working_dir) |
| output = Path("hello.txt") |
| base_command = ["touch", str(output)] |
| p = self._make_main_parser() |
| export_dir = Path("saved-diffs") # relative to working dir |
| export_dir_abs = working_dir / export_dir |
| main_args, other = p.parse_known_args( |
| [ |
| "--check-determinism", |
| "--local", |
| # request that differences be saved to an export dir |
| f"--miscomparison-export-dir={export_dir}", |
| "--", |
| ] |
| + base_command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| output_files=[output], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertTrue(action.remote_disable) |
| self.assertEqual( |
| action.miscomparison_export_dir, working_dir / export_dir |
| ) |
| |
| with mock.patch.object( |
| cl_utils, |
| "subprocess_call", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run_with_main_args(main_args) |
| |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| launch_command = args[0] |
| self.assertEqual(kwargs["cwd"], working_dir) |
| check_prefix, sep, main_command = cl_utils.partition_sequence( |
| launch_command, "--" |
| ) |
| self.assertEqual(check_prefix[0], sys.executable) |
| self.assertIn( |
| str(exec_root_rel / fuchsia._CHECK_DETERMINISM_SCRIPT), check_prefix |
| ) |
| self.assertIn("--check-repeatability", check_prefix) |
| # Make sure export dir argument is forwarded. |
| export_out_dir = export_dir_abs / build_dir |
| self.assertIn( |
| f"--miscomparison-export-dir={export_out_dir}", check_prefix |
| ) |
| _, _, output_list = cl_utils.partition_sequence( |
| check_prefix, "--outputs" |
| ) |
| self.assertEqual(output_list, [str(output)]) |
| self.assertEqual(main_command, base_command) |
| |
| def test_output_leak_scan_with_canonical_working_dir_mocked(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| canonical_dir_option = "--canonicalize_working_dir=true" |
| p = self._make_main_parser() |
| command = ["echo"] |
| main_args, other = p.parse_known_args( |
| [canonical_dir_option, "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertTrue(action.canonicalize_working_dir) |
| self.assertIn(canonical_dir_option, action.options) |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_scan: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_scan.assert_called_with( |
| paths=[], |
| command=command, |
| pattern=output_leak_scanner.PathPattern(action.build_subdir), |
| ) |
| mock_run.assert_called() |
| |
| def test_output_leak_scan_skipped_when_build_subdir_is_dot(self): |
| exec_root = Path("/home/project") |
| working_dir = exec_root # build_subdir == '.' |
| canonical_dir_option = "--canonicalize_working_dir=true" |
| p = self._make_main_parser() |
| command = ["echo"] |
| main_args, other = p.parse_known_args( |
| [canonical_dir_option, "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertTrue(action.canonicalize_working_dir) |
| self.assertIn(canonical_dir_option, action.options) |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_scan: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_scan.assert_not_called() |
| mock_run.assert_called() |
| |
| def test_output_leak_scan_with_canonical_working_dir_called(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| canonical_dir_option = "--canonicalize_working_dir=true" |
| p = self._make_main_parser() |
| command = ["echo"] |
| main_args, other = p.parse_known_args( |
| [canonical_dir_option, "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertTrue(action.canonicalize_working_dir) |
| self.assertIn(canonical_dir_option, action.options) |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| |
| def test_output_leak_scan_with_error_stops_run(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| canonical_dir_option = "--canonicalize_working_dir=true" |
| p = self._make_main_parser() |
| command = ["echo", str(build_dir)] # command leaks build_dir |
| main_args, other = p.parse_known_args( |
| [canonical_dir_option, "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertTrue(action.canonicalize_working_dir) |
| self.assertIn(canonical_dir_option, action.options) |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 1) # due to output_leak_scanner |
| # The output_leak_scan error stopped execution. |
| mock_run.assert_not_called() |
| |
| |
| class RemoteActionFlagParserTests(unittest.TestCase): |
| def _forward_and_parse(self, command): |
| forwarded, filtered = remote_action.forward_remote_flags( |
| ["--"] + command |
| ) |
| main_args, unknown = remote_action._MAIN_ARG_PARSER.parse_known_args( |
| forwarded |
| ) |
| return main_args, unknown, filtered |
| |
| def test_defaults(self): |
| remote_args, unknown, other = self._forward_and_parse([]) |
| self.assertFalse(remote_args.local) |
| self.assertEqual(remote_args.inputs, []) |
| self.assertEqual(remote_args.output_files, []) |
| self.assertEqual(remote_args.output_directories, []) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, []) |
| |
| def test_command_without_forwarding(self): |
| command = [ |
| "clang++", |
| "--target=powerpc-apple-darwin8", |
| "-fcrash-diagnostics-dir=nothing/to/see/here", |
| "-c", |
| "hello.cxx", |
| "-o", |
| "hello.o", |
| ] |
| remote_args, unknown, other = self._forward_and_parse(command) |
| self.assertFalse(remote_args.local) |
| self.assertEqual(remote_args.inputs, []) |
| self.assertEqual(remote_args.output_files, []) |
| self.assertEqual(remote_args.output_directories, []) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, command) |
| |
| def test_disable(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| ["cat", "foo.txt", "--remote-disable"] |
| ) |
| self.assertTrue(remote_args.local) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, ["cat", "foo.txt"]) |
| |
| def test_inputs(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| [ |
| "cat", |
| "--remote-inputs=bar.txt", |
| "bar.txt", |
| "--remote-inputs=quux.txt", |
| "quux.txt", |
| ] |
| ) |
| self.assertEqual(remote_args.inputs, ["bar.txt", "quux.txt"]) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, ["cat", "bar.txt", "quux.txt"]) |
| |
| def test_inputs_comma(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| [ |
| "cat", |
| "--remote-inputs=w,x", |
| "bar.txt", |
| "--remote-inputs=y,z", |
| "quux.txt", |
| ] |
| ) |
| self.assertEqual( |
| list(cl_utils.flatten_comma_list(remote_args.inputs)), |
| ["w", "x", "y", "z"], |
| ) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, ["cat", "bar.txt", "quux.txt"]) |
| |
| def test_output_files_comma(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| [ |
| "./generate.sh", |
| "--remote-outputs=w,x", |
| "bar.txt", |
| "--remote-outputs=y,z", |
| "quux.txt", |
| ] |
| ) |
| self.assertEqual( |
| list(cl_utils.flatten_comma_list(remote_args.output_files)), |
| ["w", "x", "y", "z"], |
| ) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, ["./generate.sh", "bar.txt", "quux.txt"]) |
| |
| def test_output_dirs_comma(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| [ |
| "./generate_dirs.sh", |
| "--remote-output-dirs=w,x", |
| "bar.txt", |
| "--remote-output-dirs=y,z", |
| "quux.txt", |
| ] |
| ) |
| self.assertEqual( |
| list(cl_utils.flatten_comma_list(remote_args.output_directories)), |
| ["w", "x", "y", "z"], |
| ) |
| self.assertEqual(unknown, []) |
| self.assertEqual(other, ["./generate_dirs.sh", "bar.txt", "quux.txt"]) |
| |
| def test_flags(self): |
| remote_args, unknown, other = self._forward_and_parse( |
| [ |
| "cat", |
| "--remote-flag=--foo=bar", |
| "bar.txt", |
| "--remote-flag=--opt=quux", |
| "quux.txt", |
| ] |
| ) |
| self.assertEqual(unknown, ["--foo=bar", "--opt=quux"]) |
| self.assertEqual(other, ["cat", "bar.txt", "quux.txt"]) |
| |
| |
| class RemoteActionConstructionTests(unittest.TestCase): |
| _PROJECT_ROOT = Path("/my/project/root") |
| _WORKING_DIR = _PROJECT_ROOT / "build_dir" |
| |
| @property |
| def _rewrapper(self) -> Path: |
| return Path("/path/to/rewrapper") |
| |
| def _make_remote_action( |
| self, |
| rewrapper=None, |
| command=None, |
| exec_root=None, |
| working_dir=None, |
| **kwargs, # RemoteAction params |
| ) -> remote_action.RemoteAction: |
| """Create a RemoteAction for testing with some defaults.""" |
| return remote_action.RemoteAction( |
| rewrapper=rewrapper or self._rewrapper, |
| command=command, |
| exec_root=exec_root or self._PROJECT_ROOT, |
| working_dir=working_dir or self._WORKING_DIR, |
| **kwargs, |
| ) |
| |
| def test_minimal(self): |
| command = ["cat", "meow.txt"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.remote_only_command, command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| self.assertEqual(action.exec_root_rel, Path("..")) |
| self.assertFalse(action.save_temps) |
| self.assertFalse(action.remote_disable) |
| self.assertEqual(action.build_subdir, Path("build_dir")) |
| self.assertEqual( |
| action.launch_command, |
| [str(self._rewrapper), f"--exec_root={self._PROJECT_ROOT}", "--"] |
| + command, |
| ) |
| self.assertFalse(action.compare_with_local) |
| self.assertFalse(action.check_determinism) |
| self.assertFalse(action.diagnose_nonzero) |
| self.assertTrue(action.download_outputs) |
| |
| def test_path_setup_implicit(self): |
| command = ["beep", "boop"] |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/not-default") |
| fake_cwd = fake_root / fake_builddir |
| with mock.patch.object(os, "curdir", fake_cwd): |
| with mock.patch.object(remote_action, "PROJECT_ROOT", fake_root): |
| action = remote_action.RemoteAction( |
| rewrapper=self._rewrapper, |
| command=command, |
| ) |
| self.assertEqual(action.exec_root, fake_root) |
| self.assertEqual(action.exec_root_rel, Path("../..")) |
| self.assertEqual(action.build_subdir, fake_builddir) |
| |
| def test_path_setup_explicit_exec_root(self): |
| command = ["beep", "boop"] |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/not-default") |
| fake_cwd = fake_root / fake_builddir |
| with mock.patch.object(os, "curdir", fake_cwd): |
| action = remote_action.RemoteAction( |
| rewrapper=self._rewrapper, |
| command=command, |
| exec_root=fake_root, |
| ) |
| self.assertEqual(action.exec_root, fake_root) |
| self.assertEqual(action.exec_root_rel, Path("../..")) |
| self.assertEqual(action.build_subdir, fake_builddir) |
| |
| def test_path_setup_explicit_exec_root_and_working_dir(self): |
| command = ["beep", "boop"] |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/not-default") |
| fake_cwd = fake_root / fake_builddir |
| action = remote_action.RemoteAction( |
| rewrapper=self._rewrapper, |
| command=command, |
| exec_root=fake_root, |
| working_dir=fake_cwd, |
| ) |
| self.assertEqual(action.exec_root, fake_root) |
| self.assertEqual(action.exec_root_rel, Path("../..")) |
| self.assertEqual(action.build_subdir, fake_builddir) |
| self.assertEqual(action.working_dir, fake_cwd) |
| self.assertFalse(action.canonicalize_working_dir) |
| self.assertEqual(action.remote_build_subdir, fake_builddir) |
| self.assertEqual( |
| action.remote_working_dir, |
| remote_action._REMOTE_PROJECT_ROOT / fake_builddir, |
| ) |
| |
| def test_path_setup_explicit_canonicalize_working_dir(self): |
| command = ["b33p", "b00p"] |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/not-default") |
| fake_cwd = fake_root / fake_builddir |
| action = remote_action.RemoteAction( |
| rewrapper=self._rewrapper, |
| options=["--canonicalize_working_dir=true"], |
| command=command, |
| exec_root=fake_root, |
| working_dir=fake_cwd, |
| ) |
| self.assertEqual(action.exec_root, fake_root) |
| self.assertEqual(action.exec_root_rel, Path("../..")) |
| self.assertEqual(action.build_subdir, fake_builddir) |
| self.assertEqual(action.working_dir, fake_cwd) |
| self.assertTrue(action.canonicalize_working_dir) |
| remote_builddir = Path("set_by_reclient/a") |
| self.assertEqual(action.remote_build_subdir, remote_builddir) |
| self.assertEqual( |
| action.remote_working_dir, |
| remote_action._REMOTE_PROJECT_ROOT / remote_builddir, |
| ) |
| |
| def test_inputs_outputs(self): |
| command = ["cat", "../src/meow.txt"] |
| action = self._make_remote_action( |
| command=command, |
| inputs=_paths(["../src/meow.txt"]), |
| output_files=_paths(["obj/woof.txt"]), |
| output_dirs=_paths([".debug"]), |
| ) |
| self.assertEqual(action.build_subdir, Path("build_dir")) |
| self.assertEqual( |
| action.inputs_relative_to_project_root, _paths(["src/meow.txt"]) |
| ) |
| self.assertEqual( |
| action.output_files_relative_to_project_root, |
| _paths(["build_dir/obj/woof.txt"]), |
| ) |
| self.assertEqual( |
| action.output_dirs_relative_to_project_root, |
| _paths(["build_dir/.debug"]), |
| ) |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_generated_inputs_list_file", |
| return_value=Path("obj/woof.txt.inputs"), |
| ) as mock_input_list_file: |
| self.assertEqual( |
| action.launch_command, |
| [ |
| "/path/to/rewrapper", |
| f"--exec_root={self._PROJECT_ROOT}", |
| "--input_list_paths=obj/woof.txt.inputs", |
| "--output_files=build_dir/obj/woof.txt", |
| "--output_directories=build_dir/.debug", |
| "--", |
| "cat", |
| "../src/meow.txt", |
| ], |
| ) |
| mock_input_list_file.assert_called_once() |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), 0) |
| mock_call.assert_called_once() |
| mock_cleanup.assert_called_once() |
| |
| def test_save_temps(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action( |
| command=command, |
| save_temps=True, |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| self.assertTrue(action.save_temps) |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), 0) |
| mock_call.assert_called_once() |
| mock_cleanup.assert_not_called() |
| |
| def test_flag_forwarding_pass_through_remote(self): |
| # RemoteAction construction no longer forwards --remote-* flags; |
| # that responsibility has been moved to |
| # remote_action.forward_remote_flags(). |
| command = [ |
| "cat", |
| "--remote-flag=--exec_strategy=racing", |
| "../src/cow/moo.txt", |
| ] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.local_only_flags, []) |
| self.assertEqual(action.options, []) |
| |
| def test_local_only_flag_forwarding(self): |
| local_file = Path("local_preamble.txt") |
| command = [ |
| "cat", |
| f"--local-only={local_file}", |
| "main.txt", |
| ] |
| output_files = [Path("out/banner.txt")] |
| action = self._make_remote_action( |
| command=command, output_files=output_files |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.local_only_flags, [str(local_file)]) |
| self.assertIn(str(local_file), action.local_wrapper_text) |
| self.assertEqual(action.options, []) |
| rewrapper_prefix = list(action._generate_remote_command_prefix()) |
| # --local-only options are sifted into the --local_wrapper script, |
| # which is generated and cleaned up. |
| self.assertIn("--local_wrapper=./out/banner.local.sh", rewrapper_prefix) |
| |
| def test_relativize_local_deps(self): |
| exec_root = Path("/exec/root") |
| working_dir = exec_root / "work" |
| action = self._make_remote_action( |
| command=["cat"], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual( |
| action._relativize_remote_or_local_deps( |
| str(exec_root / "project" / "include" / "foo.h") |
| ), |
| "../project/include/foo.h", |
| ) |
| self.assertEqual( |
| action._relativize_remote_or_local_deps( |
| str(working_dir / "gen" / "include" / "foo.h") |
| ), |
| "gen/include/foo.h", |
| ) |
| |
| def test_relativize_remote_deps(self): |
| exec_root = Path("/exec/root") |
| working_dir = exec_root / "work" / "out" |
| action = self._make_remote_action( |
| command=["cat"], |
| exec_root=exec_root, |
| working_dir=working_dir, |
| ) |
| self.assertEqual( |
| action._relativize_remote_or_local_deps( |
| str( |
| remote_action._REMOTE_PROJECT_ROOT |
| / "project" |
| / "include" |
| / "foo.h" |
| ) |
| ), |
| "../../project/include/foo.h", |
| ) |
| self.assertEqual( |
| action._relativize_remote_or_local_deps( |
| str( |
| remote_action._REMOTE_PROJECT_ROOT |
| / "work" |
| / "out" |
| / "jen" |
| / "project" |
| / "include" |
| / "foo.h" |
| ) |
| ), |
| "jen/project/include/foo.h", |
| ) |
| |
| def test_remote_fail_no_retry(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| for exit_code in (1, 2): |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(exit_code), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), exit_code) |
| |
| mock_cleanup.assert_called_once() |
| mock_call.assert_called_once() # no retry |
| |
| def test_local_fail_no_retry(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action( |
| command=command, |
| disable=True, # local-only |
| ) |
| self.assertTrue(action.remote_disable) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.launch_command, command) # no rewrapper |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| exit_code = 4 |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(exit_code), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), exit_code) |
| |
| mock_cleanup.assert_called_once() |
| mock_call.assert_called_once() # no retry |
| |
| def test_file_not_found_no_retry(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| exit_code = 2 |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult( |
| returncode=exit_code, |
| stderr=["ERROR: file not found: /bin/smash", "going home now"], |
| ), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), exit_code) |
| |
| mock_cleanup.assert_called_once() |
| mock_call.assert_called_once() # no retry |
| |
| def test_fail_to_dial_retry(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| exit_code = 5 |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult( |
| returncode=exit_code, |
| stderr=[ |
| "F0424 15:20:57.829003 1410923 main.go:112] Fail to dial unix:///b/s/w/ir/x/w/recipe_cleanup/rbedt_5k30r/reproxy.sock: context deadline exceeded", |
| "other uninteresting log message", |
| ], |
| ), |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), exit_code) |
| |
| mock_cleanup.assert_called_once() |
| mock_call.assert_called() |
| self.assertEqual(len(mock_call.call_args_list), 2) |
| |
| def test_retry_once_successful(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| for exit_code in remote_action._RETRIABLE_REWRAPPER_STATUSES: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| side_effect=[ |
| # If at first you don't succeed, |
| cl_utils.SubprocessResult(exit_code), |
| # try, try again (and succeed). |
| cl_utils.SubprocessResult(0), |
| ], |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), 0) |
| |
| mock_cleanup.assert_called_once() |
| # expect called twice, second time is the retry |
| self.assertEqual(len(mock_call.call_args_list), 2) |
| |
| def test_retry_once_fails_again(self): |
| command = ["echo", "hello"] |
| action = self._make_remote_action(command=command) |
| self.assertEqual(action.local_only_command, command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| for exit_code in remote_action._RETRIABLE_REWRAPPER_STATUSES: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| side_effect=[ |
| # If at first you don't succeed, |
| cl_utils.SubprocessResult(exit_code), |
| # try, try again (and fail again). |
| cl_utils.SubprocessResult(exit_code), |
| ], |
| ) as mock_call: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| self.assertEqual(action.run(), exit_code) # fail |
| |
| mock_cleanup.assert_called_once() |
| # expect called twice, second time is the retry |
| self.assertEqual(len(mock_call.call_args_list), 2) |
| |
| def _test_local_execution_strategy( |
| self, exec_strategy: str, local_status: int |
| ): |
| remote_command = ["echo", "hello"] |
| local_command = ["repeat-after-me", "hello"] |
| action = self._make_remote_action( |
| command=remote_command, |
| local_only_command=local_command, |
| exec_strategy=exec_strategy, |
| ) |
| self.assertEqual(action.local_only_command, local_command) |
| self.assertEqual(action.remote_only_command, remote_command) |
| self.assertEqual(action.exec_root, self._PROJECT_ROOT) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(2), |
| ) as mock_remote: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_locally", |
| return_value=local_status, |
| ) as mock_local: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "downloader", |
| return_value=_FAKE_DOWNLOADER, |
| ) as mock_downloader: |
| self.assertEqual( |
| action.run(), local_status |
| ) # fallback success |
| |
| mock_remote.assert_called_with() |
| mock_local.assert_called_with() |
| mock_cleanup.assert_called_with() |
| mock_downloader.assert_called_once_with() |
| |
| def test_strategy_local_fallback_different_command_succeeds(self): |
| self._test_local_execution_strategy( |
| exec_strategy="remote_local_fallback", |
| local_status=0, |
| ) |
| |
| def test_strategy_local_fallback_different_command_fails(self): |
| self._test_local_execution_strategy( |
| exec_strategy="remote_local_fallback", |
| local_status=1, |
| ) |
| |
| def test_strategy_local_only_different_command_succeeds(self): |
| self._test_local_execution_strategy( |
| exec_strategy="local", |
| local_status=0, |
| ) |
| |
| def test_strategy_local_only_different_command_fails(self): |
| self._test_local_execution_strategy( |
| exec_strategy="local", |
| local_status=3, |
| ) |
| |
| |
| def _fake_downloader() -> remotetool.RemoteTool: |
| return remotetool.RemoteTool( |
| reproxy_cfg={ |
| "service": "foo.buildservice:443", |
| "instance": "my-project/remote/instances/default", |
| } |
| ) |
| |
| |
| _FAKE_DOWNLOADER = _fake_downloader() |
| |
| |
| class DownloadStubsTests(unittest.TestCase): |
| def test_create_stub_for_nonexistent_ignored(self): |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| p = Path("crash_logs/optional-log.txt") |
| (tdp / p.parent).mkdir(parents=True, exist_ok=True) |
| rrpl = tdp / "action_log.rrpl" |
| rrpl_contents = """ |
| command: { |
| } |
| remote_metadata: { |
| action_digest: "bef09123babc23/2037" |
| } |
| """ |
| _write_file_contents(rrpl, rrpl_contents) |
| build_id = "xyzzy" |
| log_record = remote_action.ReproxyLogEntry.parse_action_log(rrpl) |
| stub_infos = log_record.make_download_stubs( |
| files=[p], dirs=[], build_id=build_id |
| ) |
| # `p` was an optional output that was not created by the action. |
| self.assertEqual(stub_infos, {}) |
| |
| def test_create_file_stub_and_download(self): |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| p = Path("dir/big-file.txt") |
| (tdp / p.parent).mkdir(parents=True, exist_ok=True) |
| digest = "abc123abc123/343" |
| rrpl = tdp / "action_log.rrpl" |
| rrpl_contents = f""" |
| command: {{ |
| }} |
| remote_metadata: {{ |
| action_digest: "feedfacefeedface/1337" |
| output_file_digests: {{ |
| key: "{p}" |
| value: "{digest}" |
| }} |
| }} |
| """ |
| _write_file_contents(rrpl, rrpl_contents) |
| build_id = "xyzzy" |
| log_record = remote_action.ReproxyLogEntry.parse_action_log(rrpl) |
| stub_infos = log_record.make_download_stubs( |
| files=[p], dirs=[], build_id=build_id |
| ) |
| |
| self.assertEqual(len(stub_infos), 1) |
| stub_infos[p].create(tdp) |
| |
| destination = tdp / p |
| mode = destination.stat().st_mode |
| self.assertTrue(remote_action.is_download_stub_file(destination)) |
| |
| def fake_download_file( |
| downloader_self, path: Path, digest: str, **kwargs |
| ): |
| (tdp / path).write_text("hello\n") |
| return cl_utils.SubprocessResult(0) |
| |
| with mock.patch.object( |
| remotetool.RemoteTool, "download_blob", new=fake_download_file |
| ) as mock_download: |
| with mock.patch.object(Path, "rename") as mock_rename: |
| remote_action.download_from_stub_path( |
| destination, |
| downloader=_FAKE_DOWNLOADER, |
| working_dir_abs=tdp, |
| ) |
| |
| mock_rename.assert_called_with(destination) |
| self.assertEqual(destination.stat().st_mode, mode) |
| |
| def test_create_directory_stub_and_download(self): |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| p = Path("bag/of/goodies") |
| (tdp / p.parent).mkdir(parents=True, exist_ok=True) |
| digest = "09ab9c86d8f001a/6540" |
| rrpl = tdp / "action_log-2.rrpl" |
| rrpl_contents = f""" |
| command: {{ |
| }} |
| remote_metadata: {{ |
| action_digest: "cafef00dcafef00d/656" |
| output_directory_digests: {{ |
| key: "{p}" |
| value: "{digest}" |
| }} |
| }} |
| """ |
| _write_file_contents(rrpl, rrpl_contents) |
| build_id = "yzzyx" |
| log_record = remote_action.ReproxyLogEntry.parse_action_log(rrpl) |
| stub_infos = log_record.make_download_stubs( |
| files=[], dirs=[p], build_id=build_id |
| ) |
| self.assertEqual(len(stub_infos), 1) |
| stub_infos[p].create(tdp) |
| |
| destination = tdp / p |
| self.assertTrue(remote_action.is_download_stub_file(destination)) |
| |
| def fake_download_dir( |
| downloader_self, path: Path, digest: str, **kwargs |
| ): |
| (tdp / path).mkdir() |
| (tdp / path / "readme.txt").write_text("hello\n") |
| return cl_utils.SubprocessResult(0) |
| |
| with mock.patch.object( |
| remotetool.RemoteTool, "download_dir", new=fake_download_dir |
| ) as mock_download: |
| with mock.patch.object(Path, "rename") as mock_rename: |
| remote_action.download_from_stub_path( |
| destination, |
| downloader=_FAKE_DOWNLOADER, |
| working_dir_abs=tdp, |
| ) |
| mock_rename.assert_called_with(destination) |
| |
| def test_read_fail(self): |
| with tempfile.TemporaryDirectory() as td: |
| stub_file = Path(td) / "testing.stub" |
| _write_file_contents(stub_file, "#!/bin/sh\nnot a stub file\n") |
| with self.assertRaises(remote_action.DownloadStubFormatError): |
| remote_action.DownloadStubInfo.read_from_file(stub_file) |
| |
| def test_stub_write_read_match(self): |
| stub = remote_action.DownloadStubInfo( |
| path=Path("foo/bar.baz"), |
| type="file", |
| blob_digest="abc00101ef/240", |
| action_digest="08871bc3d1/18", |
| build_id="random-id888", |
| ) |
| with tempfile.TemporaryDirectory() as td: |
| stub_file = Path(td) / "identity.stub" |
| stub._write(stub_file) |
| new_stub = remote_action.DownloadStubInfo.read_from_file(stub_file) |
| |
| self.assertEqual(stub, new_stub) |
| |
| def test_create(self): |
| path = Path("foo/goes/deeper/bar.baz") |
| stub = remote_action.DownloadStubInfo( |
| path=path, |
| type="file", |
| blob_digest="abc00101ef/240", |
| action_digest="08871bc3d1/18", |
| build_id="random-id888", |
| ) |
| with tempfile.TemporaryDirectory() as td: |
| full_path = td / path |
| stub.create(working_dir_abs=Path(td)) |
| self.assertTrue(remote_action.is_download_stub_file(full_path)) |
| read_back = remote_action.DownloadStubInfo.read_from_file(full_path) |
| self.assertEqual(read_back, stub) |
| |
| def test_download_to_alt_dest(self): |
| blob_digest = "00111ddeee000aa/24" |
| stub = remote_action.DownloadStubInfo( |
| path=Path("foo/bar.baz"), |
| type="file", |
| blob_digest=blob_digest, |
| action_digest="bce876da011112/14", |
| build_id="random-id777", |
| ) |
| working_dir = Path("/root/work") |
| dest = Path("some/where/else.baz") |
| download_status = 0 |
| with mock.patch.object( |
| remotetool.RemoteTool, |
| "download_blob", |
| return_value=cl_utils.SubprocessResult(download_status), |
| ) as mock_download: |
| with mock.patch.object(Path, "rename") as mock_rename: |
| with mock.patch.object(Path, "chmod") as mock_chmod: |
| with mock.patch.object(Path, "stat") as mock_stat: |
| with mock.patch.object( |
| remote_action, |
| "is_download_stub_file", |
| return_value=False, |
| ) as mock_is_stub: |
| status = stub.download( |
| downloader=_FAKE_DOWNLOADER, |
| working_dir_abs=working_dir, |
| dest=dest, |
| ) |
| self.assertEqual(status.returncode, download_status) |
| mock_download.assert_called_with( |
| path=remote_action.download_temp_location(working_dir / dest), |
| digest=blob_digest, |
| cwd=working_dir, |
| ) |
| mock_stat.assert_called() |
| mock_chmod.assert_called() |
| mock_is_stub.assert_called_once() |
| mock_rename.assert_called_with(working_dir / dest) |
| |
| def test_download_to_alt_dest_preserving_backup_stub(self): |
| blob_digest = "00111ddeee000aa/24" |
| stub = remote_action.DownloadStubInfo( |
| path=Path("foo/bar.baz"), |
| type="file", |
| blob_digest=blob_digest, |
| action_digest="bce876da011112/14", |
| build_id="random-id777", |
| ) |
| working_dir = Path("/root/work") |
| dest = Path("some/where/else.baz") |
| download_status = 0 |
| with mock.patch.object( |
| remotetool.RemoteTool, |
| "download_blob", |
| return_value=cl_utils.SubprocessResult(download_status), |
| ) as mock_download: |
| with mock.patch.object(Path, "rename") as mock_rename: |
| with mock.patch.object(Path, "chmod") as mock_chmod: |
| with mock.patch.object(Path, "stat") as mock_stat: |
| with mock.patch.object( |
| remote_action, |
| "is_download_stub_file", |
| return_value=True, |
| ) as mock_is_stub: |
| status = stub.download( |
| downloader=_FAKE_DOWNLOADER, |
| working_dir_abs=working_dir, |
| dest=dest, |
| ) |
| self.assertEqual(status.returncode, download_status) |
| mock_download.assert_called_with( |
| path=remote_action.download_temp_location(working_dir / dest), |
| digest=blob_digest, |
| cwd=working_dir, |
| ) |
| mock_stat.assert_called() |
| mock_chmod.assert_called() |
| mock_is_stub.assert_called_once() |
| mock_rename.assert_has_calls( |
| [ |
| mock.call( |
| remote_action.download_stub_backup_location( |
| working_dir / dest |
| ) |
| ), |
| mock.call(working_dir / dest), |
| ], |
| any_order=False, # order matters |
| ) |
| |
| def test_download_fail(self): |
| stub = remote_action.DownloadStubInfo( |
| path=Path("foo/bar.baz"), |
| type="file", |
| blob_digest="abcef8712/24", |
| action_digest="0923d1/21", |
| build_id="random-id999", |
| ) |
| download_status = 1 |
| with mock.patch.object( |
| remotetool.RemoteTool, |
| "download_blob", |
| return_value=cl_utils.SubprocessResult(download_status), |
| ) as mock_download: |
| with mock.patch.object(Path, "rename") as mock_rename: |
| status = stub.download( |
| downloader=_FAKE_DOWNLOADER, |
| working_dir_abs=Path("/root/work"), |
| ) |
| self.assertEqual(status.returncode, download_status) |
| mock_download.assert_called_once() |
| mock_rename.assert_not_called() |
| |
| def test_made_download_stubs_for_remote_execution(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| output = "out.out" |
| main_args, other = p.parse_known_args([download_option, "--"] + command) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.932874" |
| fake_log_record = FakeReproxyLogEntry(completion_status="SUCCESS") |
| with mock.patch.object( |
| remote_action, "_reproxy_log_dir", return_value=logdir |
| ) as mock_log_dir: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, |
| "parse_action_log", |
| return_value=fake_log_record, |
| ) as mock_parse_log: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "make_download_stubs" |
| ) as mock_stub: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| mock_log_dir.assert_called_once() |
| mock_parse_log.assert_called_with(Path(output + ".rrpl")) |
| mock_stub.assert_called_with( |
| files=[Path(output)], |
| dirs=[], |
| build_id=Path(logdir).name, |
| ) |
| |
| def test_made_download_stubs_for_racing_remote_win(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| output = "out.out" |
| main_args, other = p.parse_known_args( |
| [download_option, "--exec_strategy=racing", "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.932875" |
| fake_log_record = FakeReproxyLogEntry( |
| completion_status="STATUS_CACHE_HIT" |
| ) |
| with mock.patch.object( |
| remote_action, "_reproxy_log_dir", return_value=logdir |
| ) as mock_log_dir: |
| with mock.patch.object( |
| remote_action.RemoteAction, "download_inputs", return_value={} |
| ) as mock_download_inputs: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, |
| "parse_action_log", |
| return_value=fake_log_record, |
| ) as mock_parse_log: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "make_download_stubs" |
| ) as mock_stub: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| mock_download_inputs.assert_called_once() |
| mock_log_dir.assert_called_once() |
| mock_parse_log.assert_called_with(Path(output + ".rrpl")) |
| mock_stub.assert_called_with( |
| files=[Path(output)], |
| dirs=[], |
| build_id=Path(logdir).name, |
| ) |
| |
| def test_download_inputs_for_local_execution(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| input_file = Path("in.in") # pretend this is a download stub |
| output = "out.out" |
| main_args, other = p.parse_known_args( |
| ["--exec_strategy=local", "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| inputs=[Path(input_file)], |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| options = action.options |
| |
| fake_stub_info = remote_action.DownloadStubInfo( |
| path=input_file, |
| type="file", |
| blob_digest="8760ad0b/992", |
| action_digest="12987e0d8a77/43", |
| build_id="random-id12391", |
| ) |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "downloader", |
| return_value=_FAKE_DOWNLOADER, |
| ) as mock_downloader: |
| with mock.patch.object( |
| remote_action, |
| "download_input_stub_paths_batch", |
| return_value={input_file: cl_utils.SubprocessResult(0)}, |
| ) as mock_download: |
| download_statuses = action.download_inputs(lambda path: True) |
| |
| self.assertIn(input_file, download_statuses) |
| self.assertEqual(download_statuses[input_file].returncode, 0) |
| mock_downloader.assert_called_once_with() |
| mock_download.assert_called_once() |
| |
| def test_no_download_stubs_for_local_execution(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| output = "out.out" |
| main_args, other = p.parse_known_args( |
| [download_option, "--exec_strategy=local", "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.888222" |
| fake_log_record = FakeReproxyLogEntry( |
| completion_status="STATUS_LOCAL_EXECUTION" |
| ) |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, |
| "parse_action_log", |
| return_value=fake_log_record, |
| ) as mock_parse_log: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "make_download_stubs" |
| ) as mock_stub: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "download_inputs", |
| return_value={}, |
| ) as mock_download_inputs: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| mock_download_inputs.assert_called_once() |
| mock_parse_log.assert_called_with(Path(output + ".rrpl")) |
| mock_stub.assert_not_called() |
| |
| def test_no_download_stubs_for_racing_local_win(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| output = "out.out" |
| main_args, other = p.parse_known_args( |
| [download_option, "--exec_strategy=racing", "--"] + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.213123" |
| fake_log_record = FakeReproxyLogEntry( |
| completion_status="STATUS_RACING_LOCAL" |
| ) |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, |
| "parse_action_log", |
| return_value=fake_log_record, |
| ) as mock_parse_log: |
| with mock.patch.object( |
| remote_action.RemoteAction, "download_inputs", return_value={} |
| ) as mock_download_inputs: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "make_download_stubs" |
| ) as mock_stub: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| mock_download_inputs.assert_called_once() |
| mock_parse_log.assert_called_with(Path(output + ".rrpl")) |
| mock_stub.assert_not_called() |
| |
| def test_no_download_stubs_for_local_fallback(self): |
| exec_root = Path("/home/project") |
| build_dir = Path("build-out") |
| working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| output = "out.out" |
| main_args, other = p.parse_known_args( |
| [download_option, "--exec_strategy=remote_local_fallback", "--"] |
| + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| output_files=[Path(output)], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.81891" |
| fake_log_record = FakeReproxyLogEntry( |
| completion_status="STATUS_LOCAL_FALLBACK" |
| ) |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, |
| "parse_action_log", |
| return_value=fake_log_record, |
| ) as mock_parse_log: |
| with mock.patch.object( |
| remote_action.RemoteAction, "download_inputs", return_value={} |
| ) as mock_download_inputs: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "make_download_stubs" |
| ) as mock_stub: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| exit_code = action.run() |
| self.assertEqual(exit_code, 0) |
| mock_run.assert_called() |
| mock_download_inputs.assert_called_once() |
| mock_parse_log.assert_called_with(Path(output + ".rrpl")) |
| mock_stub.assert_not_called() |
| |
| def _setup_update_stub_test( |
| self, tdp: Path, output_contents: str = None |
| ) -> Tuple[remote_action.RemoteAction, FakeReproxyLogEntry]: |
| exec_root = tdp |
| build_dir = Path("build-out") |
| self.working_dir = exec_root / build_dir |
| download_option = "--download_outputs=false" |
| p = remote_action._MAIN_ARG_PARSER |
| command = ["echo"] |
| self.output = Path("out.out") |
| main_args, other = p.parse_known_args( |
| [download_option, "--preserve_unchanged_output_mtime", "--"] |
| + command |
| ) |
| action = remote_action.remote_action_from_args( |
| main_args, |
| remote_options=other, |
| exec_root=exec_root, |
| working_dir=self.working_dir, |
| output_files=[self.output], |
| ) |
| self.assertEqual(action.local_only_command, command) |
| self.assertFalse(action.download_outputs) |
| self.assertEqual(action.expected_downloads, []) |
| self.assertTrue(action.preserve_unchanged_output_mtime) |
| options = action.options |
| self.assertIn(download_option, options) |
| logdir = "/fake/tmp/rpl/logz.532874" |
| |
| if output_contents is not None: |
| self.working_dir.mkdir(parents=True, exist_ok=True) |
| (self.working_dir / self.output).write_text(output_contents) |
| output_digest = remote_action.get_blob_digest( |
| self.working_dir / self.output |
| ) |
| else: |
| output_digest = "aa55aa55/33" # fake |
| |
| fake_log_record = FakeReproxyLogEntry( |
| completion_status="SUCCESS", |
| output_file_digests={self.output: output_digest}, |
| output_directory_digests={}, |
| action_digest="765321/44", |
| ) |
| return action, fake_log_record |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_stub_matches_digest( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test(Path(td)) |
| |
| # create a pre-existing stub-file with the same digest as the new output |
| old_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| old_stub_info.create(self.working_dir) |
| self.assertTrue( |
| remote_action.is_download_stub_file( |
| self.working_dir / old_stub_info.path |
| ) |
| ) |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| action._update_stub(old_stub_info) |
| |
| mock_create_stub.assert_not_called() |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_stub_mismatches_digest( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test(Path(td)) |
| # create a pre-existing stub-file with a different digest as the new output |
| old_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| new_stub_info = copy.deepcopy(old_stub_info) |
| old_stub_info._blob_digest = "66776677/33" # mismatched digest |
| old_stub_info.create(self.working_dir) |
| self.assertTrue( |
| remote_action.is_download_stub_file( |
| self.working_dir / old_stub_info.path |
| ) |
| ) |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| action._update_stub(new_stub_info) |
| |
| mock_create_stub.assert_called_with(self.working_dir) |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_file_matches_digest_with_backup_stub( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test( |
| Path(td), output_contents="h3llo" |
| ) |
| |
| # pre-existing output file's digest matches that from the remote |
| # action, along with its backup download stub. |
| stub_location = remote_action.download_stub_backup_location( |
| self.output |
| ) |
| old_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| old_stub_info.create(self.working_dir, dest=stub_location) |
| self.assertTrue( |
| remote_action.is_download_stub_file( |
| self.working_dir / stub_location |
| ) |
| ) |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| with mock.patch.object(Path, "unlink") as mock_remove: |
| action._update_stub(old_stub_info) |
| |
| # old file (and its stub) are left untouched |
| mock_remove.assert_not_called() |
| mock_create_stub.assert_not_called() |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_file_matches_digest_without_backup_stub( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test( |
| Path(td), output_contents="h3llo" |
| ) |
| |
| # pre-existing output file's digest matches that from the remote |
| # action, without backup download stub. |
| old_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| with mock.patch.object(Path, "unlink") as mock_remove: |
| action._update_stub(old_stub_info) |
| |
| # old file is left untouched |
| mock_remove.assert_not_called() |
| mock_create_stub.assert_not_called() |
| |
| def test_make_download_stub_info_not_found(self): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test( |
| Path(td), output_contents="h3llo" |
| ) |
| # Reference some path that is not among the recorded |
| # output file/directory digests. |
| stub_info = fake_log_record.make_download_stub_info( |
| Path("some/optional/output"), build_id="new-build-id" |
| ) |
| self.assertIsNone(stub_info) |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_file_mismatches_digest_with_backup_stub( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test( |
| Path(td), output_contents="h3llo" |
| ) |
| |
| # pre-existing output file's digest does not match that from the |
| # remote action. |
| stub_location = remote_action.download_stub_backup_location( |
| self.output |
| ) |
| old_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| new_stub_info = copy.deepcopy(old_stub_info) |
| old_stub_info.create(self.working_dir, dest=stub_location) |
| new_stub_info._blob_digest = "43218765/11" # mismatched digest |
| self.assertTrue( |
| remote_action.is_download_stub_file( |
| self.working_dir / stub_location |
| ) |
| ) |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| with mock.patch.object(Path, "unlink") as mock_remove: |
| action._update_stub(new_stub_info) |
| |
| # old file is replaced with new stub, old stub is removed |
| mock_remove.assert_called_with() |
| mock_create_stub.assert_called_with(self.working_dir) |
| |
| def test_update_stub_preserve_unchanged_output_mtime_existing_file_mismatches_digest_without_backup_stub( |
| self, |
| ): |
| with tempfile.TemporaryDirectory() as td: |
| action, fake_log_record = self._setup_update_stub_test( |
| Path(td), output_contents="h3llo" |
| ) |
| # pre-existing output file's digest does not match that from the |
| # remote action. |
| new_stub_info = fake_log_record.make_download_stub_info( |
| self.output, build_id="new-build-id" |
| ) |
| new_stub_info._blob_digest = "43218765/11" # mismatched digest |
| |
| # bypass the remote action running |
| with mock.patch.object( |
| remote_action.DownloadStubInfo, "create" |
| ) as mock_create_stub: |
| with mock.patch.object(Path, "unlink") as mock_remove: |
| action._update_stub(new_stub_info) |
| |
| # old file is replaced with new stub |
| mock_remove.assert_called_with() |
| mock_create_stub.assert_called_with(self.working_dir) |
| |
| |
| class RbeDiagnosticsTests(unittest.TestCase): |
| def _make_remote_action(self, **kwargs): |
| command = ["echo", "hello"] |
| exec_root = Path("/path/to/project/root") |
| working_dir = exec_root / "build/stuff/here" |
| return remote_action.RemoteAction( |
| rewrapper="/path/to/rewrapper", |
| command=command, |
| exec_root=exec_root, |
| working_dir=working_dir, |
| **kwargs, |
| ) |
| |
| def test_analyze_conditions_positive(self): |
| action = self._make_remote_action(diagnose_nonzero=True) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(1), |
| ) as mock_run: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| with mock.patch.object( |
| remote_action, "analyze_rbe_logs" |
| ) as mock_analyze: |
| self.assertEqual(action.run(), 1) |
| |
| mock_cleanup.assert_called_once() |
| mock_run.assert_called_once() |
| mock_analyze.assert_called_once() |
| args, kwargs = mock_analyze.call_args_list[0] |
| self.assertEqual(kwargs["action_log"], action._action_log) |
| |
| def test_analyzing_not_requested(self): |
| action = self._make_remote_action(diagnose_nonzero=False) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(1), |
| ) as mock_run: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| with mock.patch.object( |
| remote_action, "analyze_rbe_logs" |
| ) as mock_analyze: |
| self.assertEqual(action.run(), 1) |
| |
| mock_cleanup.assert_called_once() |
| mock_run.assert_called_once() |
| mock_analyze.assert_not_called() |
| |
| def test_not_analyzing_on_success(self): |
| action = self._make_remote_action(diagnose_nonzero=True) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_run: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| with mock.patch.object( |
| remote_action, "analyze_rbe_logs" |
| ) as mock_analyze: |
| self.assertEqual(action.run(), 0) |
| |
| mock_cleanup.assert_called_once() |
| mock_run.assert_called_once() |
| mock_analyze.assert_not_called() |
| |
| def test_not_analyzing_local_execution(self): |
| action = self._make_remote_action( |
| diagnose_nonzero=True, |
| exec_strategy="local", |
| ) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(1), |
| ) as mock_run: |
| with mock.patch.object( |
| remote_action.RemoteAction, "download_inputs", return_value={} |
| ) as mock_download_inputs: |
| with mock.patch.object( |
| remote_action.RemoteAction, "_cleanup" |
| ) as mock_cleanup: |
| with mock.patch.object( |
| remote_action, "analyze_rbe_logs" |
| ) as mock_analyze: |
| self.assertEqual(action.run(), 1) |
| |
| mock_download_inputs.assert_called_once() |
| mock_cleanup.assert_called_once() |
| mock_run.assert_called_once() |
| mock_analyze.assert_not_called() |
| |
| def test_analyze_flow(self): |
| pid = 6789 |
| action_log = Path("obj/my_action.rrpl") |
| fake_rewrapper_logs = [ |
| f"/noisy/log/rewrapper.where.who.log.INFO.when.{pid}", |
| f"/noisy/log/rewrapper.where.who.log.ERROR.when.{pid}", |
| ] |
| unnamed_mocks = [ |
| mock.patch.object( |
| remote_action, |
| "_reproxy_log_dir", |
| return_value="/path/to/tmp/reproxy.999999", |
| ), |
| mock.patch.object( |
| remote_action, |
| "_rewrapper_log_dir", |
| return_value="/path/to/tmp/reproxy.999999/wrapper/logz", |
| ), |
| mock.patch.object(Path, "is_file", return_value=True), |
| ] |
| with contextlib.ExitStack() as stack: |
| for m in unnamed_mocks: |
| stack.enter_context(m) |
| |
| with mock.patch.object( |
| Path, "glob", return_value=fake_rewrapper_logs |
| ) as mock_glob: |
| with mock.patch.object( |
| remote_action.ReproxyLogEntry, "parse_action_log" |
| ) as mock_parse_action_log: |
| with mock.patch.object( |
| remote_action, |
| "_file_lines_matching", |
| return_value=["this is interesting"], |
| ) as mock_read_log: |
| with mock.patch.object( |
| remote_action, "_diagnose_reproxy_error_line" |
| ) as mock_diagnose_line: |
| remote_action.analyze_rbe_logs( |
| rewrapper_pid=pid, |
| action_log=action_log, |
| ) |
| mock_glob.assert_called_once() |
| mock_read_log.assert_called_once() |
| mock_parse_action_log.assert_called_with(action_log) |
| mock_diagnose_line.assert_called() |
| |
| def test_parse_reproxy_log_record_lines(self): |
| exec_id = "xx-yy-zzzz" |
| action_digest = "2afd98ae7274456b2bfc208e10f4cbe75fca88c2c41e352e57cb6b9ad840bf64/144" |
| output_path = Path("obj/sub/lib/foo.o") |
| output_digest = "345324aefg983bc0/531" |
| log_lines = f""" |
| command: {{ |
| identifiers: {{ |
| command_id: "8ea55c85-0ae36078" |
| invocation_id: "979eedda-4643-45da-af15-0d2f8531ba98" |
| tool_name: "re-client" |
| execution_id: "{exec_id}" |
| }} |
| }} |
| remote_metadata: {{ |
| command_digest: "e9d024e5dc99438b08f4592a09379e65146149821480e2057c7afc285d30f090/181" |
| action_digest: "{action_digest}" |
| output_file_digests: {{ |
| key: "{output_path}" |
| value: "{output_digest}" |
| }} |
| }} |
| """.splitlines() |
| log_entry = remote_action.ReproxyLogEntry._parse_lines(log_lines) |
| self.assertEqual(log_entry.execution_id, exec_id) |
| self.assertEqual(log_entry.action_digest, action_digest) |
| self.assertEqual( |
| log_entry.output_file_digests[output_path], output_digest |
| ) |
| |
| def test_diagnose_uninteresting_log_line(self): |
| line = "This diagnostic does not appear interesting." |
| f = io.StringIO() |
| with contextlib.redirect_stdout(f): |
| remote_action._diagnose_reproxy_error_line(line) |
| self.assertEqual(f.getvalue(), "") |
| |
| def test_diagnose_fail_to_dial(self): |
| line = "Fail to dial something something unix:///path/to/reproxy.socket" |
| f = io.StringIO() |
| with contextlib.redirect_stdout(f): |
| remote_action._diagnose_reproxy_error_line(line) |
| self.assertIn("reproxy is not running", f.getvalue()) |
| |
| def test_diagnose_rbe_permissions(self): |
| line = "Error connecting to remote execution client: rpc error: code = PermissionDenied. You have no power here!" |
| f = io.StringIO() |
| with contextlib.redirect_stdout(f): |
| remote_action._diagnose_reproxy_error_line(line) |
| self.assertIn( |
| "You might not have permssion to access the RBE instance", |
| f.getvalue(), |
| ) |
| |
| def test_diagnose_missing_input_file(self): |
| path = "../oops/did/I/forget/this.file" |
| line = f"Status:LocalErrorResultStatus ... Err:stat {path}: no such file or directory" |
| f = io.StringIO() |
| with contextlib.redirect_stdout(f): |
| remote_action._diagnose_reproxy_error_line(line) |
| self.assertIn( |
| f"missing a local input file for uploading: {path} (source)", |
| f.getvalue(), |
| ) |
| |
| |
| class MainTests(unittest.TestCase): |
| def test_help_flag(self): |
| stdout = io.StringIO() |
| # Just make sure help exits successfully, without any exceptions |
| # due to argument parsing. |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| sys, "exit", side_effect=ImmediateExit |
| ) as mock_exit: |
| with self.assertRaises(ImmediateExit): |
| remote_action.main(["--help"]) |
| mock_exit.assert_called_with(0) |
| |
| def test_auto_relaunch_with_reproxy_not_needed_for_local(self): |
| command = ["--local", "--", "echo", "hello"] |
| exit_code = 7 |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "run_with_main_args", |
| return_value=exit_code, |
| ): |
| self.assertEqual(remote_action.main(command), exit_code) |
| |
| def test_auto_relaunch_with_reproxy_not_needed_for_dry_run(self): |
| command = ["--dry-run", "--", "echo", "hello"] |
| self.assertEqual(remote_action.main(command), 0) |
| |
| def test_auto_relaunch_with_reproxy_not_needed_with_env(self): |
| command = ["--", "echo", "hello"] |
| exit_code = 9 |
| with mock.patch.object( |
| os.environ, "get", return_value="/any/value/will/do" |
| ) as mock_env: |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "run_with_main_args", |
| return_value=exit_code, |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), exit_code) |
| mock_env.assert_called() |
| mock_run.assert_called_once() |
| |
| def test_auto_relaunch_with_reproxy_needed(self): |
| command = ["--", "echo", "hello"] |
| # Expect to relaunch because the necessary env variables |
| # are absent. |
| 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): |
| remote_action.main(command) |
| 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("remote_action.py")) |
| self.assertEqual(wrapped_command, command[1:]) |
| |
| def test_main_args_remote_inputs(self): |
| command = ["--inputs", "src/in.txt", "--", "echo", "hello"] |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "run_with_main_args", return_value=0 |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), 0) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| main_args = args[0] |
| self.assertEqual(main_args.inputs, ["src/in.txt"]) |
| |
| def test_main_args_remote_inputs_repeated(self): |
| command = [ |
| "--inputs", |
| "src/in.txt", |
| "--inputs=another.s", |
| "--", |
| "echo", |
| "hello", |
| ] |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "run_with_main_args", return_value=0 |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), 0) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| main_args = args[0] |
| self.assertEqual(main_args.inputs, ["src/in.txt", "another.s"]) |
| |
| def test_main_args_local(self): |
| command = ["--local", "--", "echo", "hello"] |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "run_with_main_args", return_value=0 |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), 0) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| main_args = args[0] |
| self.assertTrue(main_args.local) |
| |
| def test_flag_forwarding_remote_disable(self): |
| command = ["--", "echo", "--remote-disable", "hello"] |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "run_with_main_args", return_value=0 |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), 0) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| main_args = args[0] |
| self.assertTrue(main_args.local) |
| |
| def test_main_args_local_check_determinism(self): |
| command = ["--local", "--check-determinism", "--", "echo", "hello"] |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| remote_action.RemoteAction, "run_with_main_args", return_value=0 |
| ) as mock_run: |
| self.assertEqual(remote_action.main(command), 0) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_once() |
| args, kwargs = mock_run.call_args_list[0] |
| main_args = args[0] |
| self.assertTrue(main_args.local) |
| self.assertTrue(main_args.check_determinism) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |