blob: ba44fcff4ba2c16421d7e012314f48efb381e002 [file] [log] [blame]
#!/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 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
_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}")
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_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_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 / remote_action._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_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)
self.assertEqual(action.always_download, [])
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(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.options, [])
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:
self.assertEqual(
action.run(), local_status) # fallback success
mock_remote.assert_called_with()
mock_local.assert_called_with()
mock_cleanup.assert_called_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,
)
class DownloadStubsTests(unittest.TestCase):
@property
def downloader(self) -> remotetool.RemoteTool:
return remotetool.RemoteTool(
reproxy_cfg={
"service": "foo.buildservice:443",
"instance": "my-project/remote/instances/default",
})
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
self.assertTrue(remote_action.is_download_stub_file(destination))
exec_root = '/exec/root'
with mock.patch.object(
remotetool.RemoteTool, 'download_blob',
return_value=cl_utils.SubprocessResult(0)) as mock_download:
with mock.patch.object(Path, 'rename') as mock_rename:
remote_action.download_from_stub(
destination,
downloader=self.downloader,
working_dir_abs=tdp)
mock_download.assert_called_with(
path=Path(str(destination) + '.download-tmp'),
digest=digest,
cwd=tdp,
)
mock_rename.assert_called_with(destination)
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))
exec_root = '/exec/root'
with mock.patch.object(
remotetool.RemoteTool, 'download_dir',
return_value=cl_utils.SubprocessResult(0)) as mock_download:
with mock.patch.object(Path, 'rename') as mock_rename:
remote_action.download_from_stub(
destination,
downloader=self.downloader,
working_dir_abs=tdp)
mock_download.assert_called_with(
path=Path(str(destination) + '.download-tmp'),
digest=digest,
cwd=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_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=self.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)
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:
with mock.patch.object(remote_action.RemoteAction,
'downloader', return_value=self.
downloader) as mock_downloader:
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=logdir,
)
mock_downloader.assert_called_with()
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)
options = action.options
self.assertIn(download_option, options)
logdir = '/fake/tmp/rpl/logz.932874'
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.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:
with mock.patch.object(remote_action.RemoteAction,
'downloader', return_value=self.
downloader) as mock_downloader:
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=logdir,
)
mock_downloader.assert_called_with()
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)
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, '_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_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)
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.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_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)
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.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_parse_log.assert_called_with(Path(output + '.rrpl'))
mock_stub.assert_not_called()
def test_explicit_always_download(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 = Path('out.out')
output_digest = '5a5a57a76a6e4e4/91'
main_args, other = p.parse_known_args(
[download_option, f'--download={output}', '--'] + command)
action = remote_action.remote_action_from_args(
main_args,
remote_options=other,
exec_root=exec_root,
working_dir=working_dir,
output_files=[output],
)
self.assertEqual(action.local_only_command, command)
self.assertFalse(action.download_outputs)
self.assertEqual(action.always_download, [output])
options = action.options
self.assertIn(download_option, options)
logdir = '/fake/tmp/rpl/logz.12387127'
action_digest = '9182731aef9ad0'
log_record = FakeReproxyLogEntry(
execution_id='000-111-22',
action_digest=action_digest,
output_file_digests={output: output_digest},
output_directory_digests={},
completion_status='STATUS_REMOTE_EXECUTION',
)
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=log_record) as mock_parse_log:
with mock.patch.object(remote_action.DownloadStubInfo,
'create') as mock_write_stub:
with mock.patch.object(remote_action.DownloadStubInfo,
'download') as mock_download:
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,
'downloader',
return_value=self.downloader
) as mock_downloader:
exit_code = action.run()
self.assertEqual(exit_code, 0)
mock_run.assert_called()
mock_log_dir.assert_called_with()
mock_write_stub.assert_called_with(working_dir)
mock_parse_log.assert_called_with(Path(str(output) + '.rrpl'))
mock_downloader.assert_called_with()
mock_download.assert_called_with(
downloader=self.downloader,
working_dir_abs=working_dir,
)
def test_explicit_always_download_with_real_proxy_logdir(self):
with tempfile.TemporaryDirectory() as td:
exec_root = Path(td) / 'exec_root'
logdir = Path(td) / 'tmp/reproxy/logs'
build_dir = Path('build-out')
working_dir = exec_root / build_dir
download_option = '--download_outputs=false'
p = remote_action._MAIN_ARG_PARSER
command = ['compilezor']
output = Path('objdir/hello-w0rld.obj')
output_digest = '3333377777ggggcccccaaaaa/717'
main_args, other = p.parse_known_args(
[download_option, f'--download={output}', '--'] + command)
action = remote_action.remote_action_from_args(
main_args,
remote_options=other,
exec_root=exec_root,
working_dir=working_dir,
output_files=[output],
)
self.assertEqual(action.local_only_command, command)
self.assertFalse(action.download_outputs)
self.assertEqual(action.always_download, [output])
options = action.options
self.assertIn(download_option, options)
action_log = Path(str(output) + '.rrpl')
self.assertIn(
f'--action_log={action_log}',
set(action._generate_remote_command_prefix()))
action_log_contents = f"""
command: {{
identifiers: {{
command_id: "2b2b2b2b2-111111"
invocation_id: "26363673-4643-7789-afad-7d2f8531ba98"
tool_name: "re-client"
execution_id: "fedefedefefdedefefe"
}}
}}
remote_metadata: {{
command_digest: "f9d024e5dc99433b08f4592309379e611461498d1480e2057c7afc285d30f097/181"
action_digest: "87bacb7fege6ha65a3300/77"
output_file_digests: {{
key: "{output}"
value: "{output_digest}"
}}
}}
completion_status: STATUS_CACHE_HIT
"""
def fake_run_remote(
unused_action: remote_action.RemoteAction
) -> cl_utils.SubprocessResult:
output.parent.mkdir(parents=True, exist_ok=True)
_write_file_contents(output, 'death-to-bash-scripts\n')
_write_file_contents(action_log, action_log_contents)
return cl_utils.SubprocessResult(0)
with mock.patch.object(remote_action, '_reproxy_log_dir',
return_value=logdir) as mock_log_dir:
with mock.patch.object(remote_action.DownloadStubInfo,
'download') as mock_download:
with mock.patch(
'remote_action.RemoteAction._run_maybe_remotely',
new=fake_run_remote) as mock_run:
with mock.patch.object(remote_action.RemoteAction,
'downloader', return_value=self.
downloader) as mock_downloader:
exit_code = action.run()
self.assertEqual(exit_code, 0)
mock_log_dir.assert_called_once()
mock_downloader.assert_called_with()
mock_download.assert_called_with(
downloader=self.downloader,
working_dir_abs=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,
'_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_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], 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, [])
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()