| #!/usr/bin/env vpython3 |
| # Copyright 2013 The LUCI Authors. All rights reserved. |
| # Use of this source code is governed under the Apache License, Version 2.0 |
| # that can be found in the LICENSE file. |
| |
| from __future__ import print_function |
| from __future__ import unicode_literals |
| |
| import base64 |
| import contextlib |
| import functools |
| import json |
| import logging |
| import os |
| import sys |
| import tempfile |
| import unittest |
| |
| import mock |
| |
| # Mutates sys.path. |
| import test_env |
| |
| # third_party/ |
| from depot_tools import auto_stub |
| |
| import cipdserver_fake |
| |
| import cas_util |
| import cipd |
| import errors |
| import local_caching |
| import run_isolated |
| from libs import luci_context |
| from utils import file_path |
| from utils import fs |
| from utils import large |
| from utils import logging_utils |
| from utils import subprocess42 |
| from utils import tools |
| |
| |
| ROOT_DIR = os.path.dirname( |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| ) |
| LUCI_GO_CLIENT_DIR = os.path.join(ROOT_DIR, "luci-go") |
| |
| |
| def json_dumps(data): |
| return json.dumps(data, sort_keys=True, separators=(",", ":")) |
| |
| |
| @contextlib.contextmanager |
| def init_named_caches_stub(_run_dir, _stats): |
| yield |
| |
| |
| def trim_caches_stub(_stats): |
| pass |
| |
| |
| class StorageFake: |
| def __init__(self, files, server_ref): |
| self._files = files.copy() |
| self._server_ref = server_ref |
| |
| def __enter__(self, *_): |
| return self |
| |
| def __exit__(self, *_): |
| pass |
| |
| @property |
| def server_ref(self): |
| return self._server_ref |
| |
| def async_fetch(self, channel, _priority, digest, _size, sink): |
| sink([self._files[digest]]) |
| channel.send_result(digest) |
| |
| def upload_items(self, items_to_upload, _verify_push): |
| # Return all except the first one. |
| return list(items_to_upload)[1:] |
| |
| |
| class RunIsolatedTestBase(auto_stub.TestCase): |
| # These tests fail with the following error |
| # 'AssertionError: Items in the first set but not the second' |
| # Need to run in sequential_test_runner.py as an executable |
| no_run = 1 |
| DISABLE_CIPD_FOR_TESTS = ["--cipd-enabled", False] |
| |
| @classmethod |
| def setUpClass(cls): |
| if not file_path.enable_symlink(): |
| raise Exception( |
| "Failed to enable symlink; this test requires it. On Windows, maybe " |
| "try running as Administrator" |
| ) |
| |
| def setUp(self): |
| super(RunIsolatedTestBase, self).setUp() |
| os.environ.pop("LUCI_CONTEXT", None) |
| os.environ["LUCI_GO_CLIENT_DIR"] = LUCI_GO_CLIENT_DIR |
| self._previous_dir = os.getcwd() |
| self.tempdir = tempfile.mkdtemp(prefix="run_isolated_test") |
| logging.debug("Temp dir: %s", self.tempdir) |
| cwd = os.path.join(self.tempdir, "cwd") |
| fs.mkdir(cwd) |
| os.chdir(cwd) |
| self.mock(run_isolated, "make_temp_dir", self.fake_make_temp_dir) |
| self.mock(run_isolated.auth, "ensure_logged_in", lambda _: None) |
| self.mock( |
| logging_utils.OptionParserWithLogging, |
| "logger_root", |
| logging.Logger("unittest"), |
| ) |
| |
| self._cipd_server = None # initialized lazily |
| |
| def tearDown(self): |
| # Remove mocks. |
| super(RunIsolatedTestBase, self).tearDown() |
| fs.chdir(self._previous_dir) |
| file_path.rmtree(self.tempdir) |
| if self._cipd_server: |
| self._cipd_server.close() |
| |
| @property |
| def cipd_server(self): |
| if not self._cipd_server: |
| self._cipd_server = cipdserver_fake.FakeCipdServer() |
| return self._cipd_server |
| |
| def fake_make_temp_dir(self, prefix, _root_dir): |
| """Predictably returns directory for run_tha_test (one per test case).""" |
| self.assertIn( |
| prefix, |
| ( |
| run_isolated.ISOLATED_OUT_DIR, |
| run_isolated.ISOLATED_RUN_DIR, |
| run_isolated.ISOLATED_TMP_DIR, |
| run_isolated._CAS_CLIENT_DIR, |
| "cipd_site_root", |
| ), |
| ) |
| temp_dir = os.path.join(self.tempdir, prefix) |
| self.assertFalse(fs.isdir(temp_dir)) |
| fs.makedirs(temp_dir) |
| return temp_dir |
| |
| def ir_dir(self, *args): |
| """Shortcut for joining path with ISOLATED_RUN_DIR. |
| |
| Where to map all files in run_isolated.run_tha_test(). |
| """ |
| return os.path.join(self.tempdir, run_isolated.ISOLATED_RUN_DIR, *args) |
| |
| def assertExpectedTree(self, expected, root_dir=None): |
| # Assume expected path are relative to root if not specified. |
| root_dir = root_dir or os.path.join(self.tempdir, "io") |
| |
| # Return True is the entries in out_dir are exactly the same as entries in |
| # expected. Return False otherwise. |
| count = 0 |
| for path in expected: |
| content = expected[path] |
| full_path = os.path.join(root_dir, path) |
| self.assertTrue(fs.exists(full_path), "%s doesn't exist" % full_path) |
| while fs.islink(full_path): |
| full_path = fs.readlink(full_path) |
| # If we expect a non-empty directory, check the entries in dir. |
| # If we expect an empty dir, its existence (checked above) is sufficient. |
| if not fs.isdir(full_path): |
| with open(full_path, "r") as f: |
| self.assertEqual(f.read(), content) |
| count += 1 |
| self.assertEqual(count, len(expected)) |
| |
| |
| class RunIsolatedTest(RunIsolatedTestBase): |
| # Mocked Popen so no subprocess is started. |
| def setUp(self): |
| super(RunIsolatedTest, self).setUp() |
| # list of func(args, **kwargs) -> retcode |
| # if the func returns None, then it's skipped. The first function to return |
| # non-None is taken as the retcode for the mocked Popen call. |
| self.popen_fakes = [] |
| self.popen_calls = [] |
| |
| self.capture_popen_env = False |
| self.capture_luci_ctx = False |
| |
| # pylint: disable=no-self-argument |
| class Popen: |
| def __init__(self2, args, **kwargs): |
| if not self.capture_popen_env: |
| kwargs.pop("env", None) |
| if self.capture_luci_ctx: |
| with open(os.environ["LUCI_CONTEXT"]) as f: |
| kwargs["luci_ctx"] = json.load(f) |
| self2.returncode = None |
| self2.args = args |
| self2.kwargs = kwargs |
| self.popen_calls.append((args, kwargs)) |
| |
| def yield_any_line(self2, timeout=None): |
| self.assertEqual(0.1, timeout) |
| return () |
| |
| def wait(self2, timeout=None): |
| if timeout is not None: |
| self.assertTrue(timeout > 0) |
| self2.returncode = 0 |
| for mock_fn in self.popen_fakes: |
| ret = mock_fn(self2.args, **self2.kwargs) |
| if ret is not None: |
| self2.returncode = ret |
| break |
| return self2.returncode |
| |
| def kill(self): |
| pass |
| |
| self.mock(subprocess42, "Popen", Popen) |
| |
| def test_copy_recusrsively(self): |
| src = os.path.join(self.tempdir, "src") |
| dst = os.path.join(self.tempdir, "dst") |
| with open(src, "w"): |
| pass |
| |
| run_isolated.copy_recursively(src, dst) |
| self.assertTrue(os.path.isfile(dst)) |
| |
| def test_copy_recusrsively_not_exist(self): |
| src = os.path.join(self.tempdir, "src") |
| dst = os.path.join(self.tempdir, "dst") |
| run_isolated.copy_recursively(src, dst) |
| self.assertFalse(os.path.exists(dst)) |
| |
| def test_get_command_env(self): |
| old_env = os.environ.copy() |
| try: |
| os.environ.pop("B", None) |
| self.assertNotIn("B", os.environ) |
| os.environ["C"] = "foo" |
| os.environ["D"] = "bar" |
| os.environ["E"] = "baz" |
| env = run_isolated.get_command_env( |
| "/a", |
| None, |
| "/b", |
| {"A": "a", "B": None, "C": None, "E": "${ISOLATED_OUTDIR}/eggs"}, |
| {"D": ["foo"]}, |
| "/spam", |
| None, |
| ) |
| self.assertNotIn("B", env) |
| self.assertNotIn("C", env) |
| if sys.platform == "win32": |
| self.assertEqual("\\b\\foo;bar", env["D"]) |
| else: |
| self.assertEqual("/b/foo:bar", env["D"]) |
| self.assertEqual(os.sep + os.path.join("spam", "eggs"), env["E"]) |
| finally: |
| os.environ.clear() |
| os.environ.update(old_env) |
| |
| @mock.patch.dict(os.environ, {"SWARMING_TASK_ID": "4242"}) |
| def test_main(self): |
| self.mock(tools, "disable_buffering", lambda: None) |
| |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "foo.exe", |
| "cmd with space", |
| "-task-id", |
| "${SWARMING_TASK_ID}", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| self.assertEqual( |
| [ |
| ( |
| [self.ir_dir("foo.exe"), "cmd with space", "-task-id", "4242"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": subprocess42.Containment(), |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| def test_main_args(self): |
| self.mock(tools, "disable_buffering", lambda: None) |
| |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "foo.exe", |
| "cmd w/ space", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| self.assertEqual( |
| [ |
| ( |
| [self.ir_dir("foo.exe"), "cmd w/ space"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": subprocess42.Containment(), |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| def _run_tha_test( |
| self, command=None, lower_priority=False, relative_cwd=None |
| ): |
| make_tree_call = [] |
| |
| def add(i, _): |
| make_tree_call.append(i) |
| |
| for i in ("make_tree_files_read_only", "make_tree_deleteable"): |
| self.mock(file_path, i, functools.partial(add, i)) |
| |
| data = run_isolated.TaskData( |
| command=command or [], |
| relative_cwd=relative_cwd, |
| cas_instance=None, |
| cas_digest=None, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir=None, |
| cas_cache_policies=None, |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=lower_priority, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| ret = run_isolated.run_tha_test(data, None) |
| self.assertEqual(0, ret) |
| return make_tree_call |
| |
| def test_run_tha_test_naked(self): |
| self._run_tha_test(command=["invalid", "command"]) |
| self.assertEqual( |
| [ |
| ( |
| [self.ir_dir("invalid"), "command"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": None, |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| def mock_popen_with_oserr(self): |
| def r(self, args, **kwargs): |
| old_init(self, args, **kwargs) |
| raise OSError("Unknown") |
| |
| old_init = self.mock(subprocess42.Popen, "__init__", r) |
| |
| def test_main_naked(self): |
| self.mock_popen_with_oserr() |
| # The most naked .isolated file that can exist. |
| self.mock(tools, "disable_buffering", lambda: None) |
| |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "invalid", |
| "command", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(1, ret) |
| self.assertEqual(1, len(self.popen_calls)) |
| self.assertEqual( |
| [ |
| ( |
| [self.ir_dir("invalid"), "command"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": subprocess42.Containment(), |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_main_naked_without_isolated(self): |
| self.mock_popen_with_oserr() |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "/bin/echo", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(1, ret) |
| self.assertEqual( |
| [ |
| ( |
| ["/bin/echo", "hello", "world"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": subprocess42.Containment(), |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_main_naked_with_account_switch(self): |
| self.capture_luci_ctx = True |
| self.mock_popen_with_oserr() |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--switch-to-account", |
| "task", |
| "--", |
| "/bin/echo", |
| "hello", |
| "world", |
| ] |
| root_ctx = { |
| "accounts": [{"id": "bot"}, {"id": "task"}], |
| "default_account_id": "bot", |
| "secret": "sekret", |
| "rpc_port": 12345, |
| } |
| with luci_context.write(local_auth=root_ctx): |
| run_isolated.main(cmd) |
| # Switched default account to task. |
| task_ctx = root_ctx.copy() |
| task_ctx["default_account_id"] = "task" |
| self.assertEqual(task_ctx, self.popen_calls[0][1]["luci_ctx"]["local_auth"]) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_main_naked_with_account_pop(self): |
| self.capture_luci_ctx = True |
| self.mock_popen_with_oserr() |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--switch-to-account", |
| "task", |
| "--", |
| "/bin/echo", |
| "hello", |
| "world", |
| ] |
| root_ctx = { |
| "accounts": [{"id": "bot"}], # only 'bot', there's no 'task' |
| "default_account_id": "bot", |
| "secret": "sekret", |
| "rpc_port": 12345, |
| } |
| with luci_context.write(local_auth=root_ctx): |
| run_isolated.main(cmd) |
| # Unset default account, since 'task' account is not defined. |
| task_ctx = root_ctx.copy() |
| task_ctx.pop("default_account_id") |
| self.assertEqual(task_ctx, self.popen_calls[0][1]["luci_ctx"]["local_auth"]) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_main_naked_leaking(self): |
| workdir = tempfile.mkdtemp() |
| try: |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--root-dir", |
| workdir, |
| "--leak-temp-dir", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--", |
| "/bin/echo", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| finally: |
| fs.rmtree(workdir) |
| |
| def test_main_naked_with_packages(self): |
| self.mock(cipd, "get_platform", lambda: "linux-amd64") |
| |
| def pins_generator(): |
| yield { |
| "": [ |
| ("infra/data/x", "badc0fee" * 5), |
| ("infra/data/y", "cafebabe" * 5), |
| ], |
| "bin": [ |
| ("infra/tools/echo/linux-amd64", "deadbeef" * 5), |
| ], |
| } |
| yield { |
| "": [ |
| ("infra/tools/luci/cas/linux-amd64", run_isolated._LUCI_GO_REVISION) |
| ], |
| } |
| |
| pins_gen = pins_generator() |
| |
| suffix = ".exe" if sys.platform == "win32" else "" |
| |
| def fake_ensure(args, **kwargs): |
| if ( |
| args[0].endswith(os.path.join("bin", "cipd" + suffix)) |
| and args[1] == "ensure" |
| and "-json-output" in args |
| ): |
| idx = args.index("-json-output") |
| with open(args[idx + 1], "w") as json_out: |
| json.dump( |
| { |
| "result": { |
| subdir: [ |
| {"package": pkg, "instance_id": ver} for pkg, ver in packages |
| ] |
| for subdir, packages in next(pins_gen).items() |
| } |
| }, |
| json_out, |
| ) |
| return 0 |
| if args[0].endswith(os.sep + "echo" + suffix): |
| return 0 |
| self.fail("unexpected: %s, %s" % (args, kwargs)) |
| return 1 |
| |
| self.popen_fakes.append(fake_ensure) |
| cipd_cache = os.path.join(self.tempdir, "cipd_cache") |
| cmd = [ |
| "--no-log", |
| "--cipd-client-version", |
| "git:wowza", |
| "--cipd-package", |
| "bin:infra/tools/echo/${platform}:latest", |
| "--cipd-package", |
| ".:infra/data/x:latest", |
| "--cipd-package", |
| ".:infra/data/y:canary", |
| "--cipd-server", |
| self.cipd_server.url, |
| "--cipd-cache", |
| cipd_cache, |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| |
| self.assertEqual(3, len(self.popen_calls)) |
| |
| # Test cipd-ensure command for installing packages. |
| cipd_ensure_cmd, _ = self.popen_calls[0] |
| self.assertEqual( |
| cipd_ensure_cmd[:2], |
| [ |
| os.path.join(cipd_cache, "bin", "cipd" + cipd.EXECUTABLE_SUFFIX), |
| "ensure", |
| ], |
| ) |
| cache_dir_index = cipd_ensure_cmd.index("-cache-dir") |
| self.assertEqual( |
| cipd_ensure_cmd[cache_dir_index + 1], os.path.join(cipd_cache, "cache") |
| ) |
| |
| # Test cipd client cache. `git:wowza` was a tag and so is cacheable. |
| self.assertEqual(len(fs.listdir(os.path.join(cipd_cache, "versions"))), 2) |
| version_file = os.path.join( |
| cipd_cache, |
| "versions", |
| "5c2ee864d65c435dfa1eb06bf4c52f58854db39392340a91fc91e88d6000d737", |
| ) |
| self.assertTrue(fs.isfile(version_file)) |
| with open(version_file) as f: |
| self.assertEqual(f.read(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") |
| |
| client_binary_file = os.path.join( |
| cipd_cache, "clients", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
| ) |
| self.assertTrue(fs.isfile(client_binary_file)) |
| |
| # Test echo call. |
| echo_cmd, _ = self.popen_calls[2] |
| self.assertTrue( |
| echo_cmd[0].endswith( |
| os.path.sep + "bin" + os.path.sep + "echo" + cipd.EXECUTABLE_SUFFIX |
| ), |
| echo_cmd[0], |
| ) |
| self.assertEqual(echo_cmd[1:], ["hello", "world"]) |
| |
| def test_main_naked_with_invalid_cas_input(self): |
| def dump_bad_digest_json(cmd, _): |
| json_path = cmd[cmd.index("-dump-json") + 1] |
| with open(json_path, "w") as fp: |
| json.dump( |
| { |
| "result": "digest_invalid", |
| "error_details": {"digest": "abcd1234/21"}, |
| }, |
| fp, |
| ) |
| |
| self.mock(run_isolated, "_run_go_cmd_and_wait", dump_bad_digest_json) |
| result_json_path = os.path.join(self.tempdir, "result.json") |
| |
| cmd = [ |
| "--json", |
| result_json_path, |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--cipd-enabled", |
| "False", |
| "--cas-digest", |
| "not_valid", |
| "--cas-instance", |
| "some_cas_instance", |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| |
| ret = run_isolated.main(cmd) |
| self.assertEqual(1, ret) |
| with open(result_json_path, "r") as fp: |
| result_json = json.load(fp) |
| |
| missing_cas = result_json["missing_cas"][0] |
| self.assertEqual("digest_invalid", missing_cas["status"]) |
| self.assertEqual("abcd1234/21", missing_cas["digest"]) |
| self.assertEqual("some_cas_instance", missing_cas["instance"]) |
| |
| def test_main_naked_with_invalid_cipd_package(self): |
| self.mock(cipd, "get_platform", lambda: "linux-amd64") |
| suffix = ".exe" if sys.platform == "win32" else "" |
| |
| def fake_ensure(args, **kwargs): |
| if ( |
| args[0].endswith(os.path.join("bin", "cipd" + suffix)) |
| and args[1] == "ensure" |
| and "-json-output" in args |
| ): |
| idx = args.index("-json-output") |
| with open(args[idx + 1], "w") as json_out: |
| json.dump( |
| { |
| "error": "failed to resolve does/not/exists/linux-amd64@latest", |
| "error_code": "invalid_version_error", |
| "error_details": { |
| "package": "does/not/exists/linux-amd64", |
| "version": "latest", |
| }, |
| "result": None, |
| }, |
| json_out, |
| ) |
| return 0 |
| if args[0].endswith(os.sep + "echo" + suffix): |
| return 0 |
| self.fail("unexpected: %s, %s" % (args, kwargs)) |
| return 1 |
| |
| self.popen_fakes.append(fake_ensure) |
| result_json_path = os.path.join(self.tempdir, "result.json") |
| cipd_cache = os.path.join(self.tempdir, "cipd_cache") |
| cmd = [ |
| "--json", |
| result_json_path, |
| "--no-log", |
| "--cipd-package", |
| "bin:does/not/exists/${platform}:latest", |
| "--cipd-server", |
| self.cipd_server.url, |
| "--cipd-cache", |
| cipd_cache, |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(1, ret) |
| with open(result_json_path, "r") as fp: |
| result_json = json.load(fp) |
| self.assertEqual(1, len(result_json["missing_cipd"])) |
| missing_cipd = result_json["missing_cipd"][0] |
| self.assertEqual( |
| "does/not/exists/linux-amd64", missing_cipd["package_name"] |
| ) |
| self.assertEqual("invalid_version_error", missing_cipd["status"]) |
| self.assertEqual("latest", missing_cipd["version"]) |
| self.assertIsNone(missing_cipd["path"]) |
| |
| def test_main_naked_with_cipd_client_no_packages(self): |
| self.mock(cipd, "get_platform", lambda: "linux-amd64") |
| |
| cipd_cache = os.path.join(self.tempdir, "cipd_cache") |
| cmd = [ |
| "--no-log", |
| "--cipd-client-version", |
| "git:wowza", |
| "--cipd-server", |
| self.cipd_server.url, |
| "--cipd-cache", |
| cipd_cache, |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--relative-cwd", |
| "a", |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| |
| pins = { |
| "": [ |
| ( |
| "infra/tools/luci/isolated/linux-amd64", |
| run_isolated._LUCI_GO_REVISION, |
| ) |
| ], |
| } |
| |
| suffix = ".exe" if sys.platform == "win32" else "" |
| |
| def fake_ensure(args, **kwargs): |
| if ( |
| args[0].endswith(os.path.join("bin", "cipd" + suffix)) |
| and args[1] == "ensure" |
| and "-json-output" in args |
| ): |
| idx = args.index("-json-output") |
| with open(args[idx + 1], "w") as json_out: |
| json.dump( |
| { |
| "result": { |
| subdir: [ |
| {"package": pkg, "instance_id": ver} for pkg, ver in packages |
| ] |
| for subdir, packages in pins.items() |
| } |
| }, |
| json_out, |
| ) |
| return 0 |
| if args[0].endswith(os.sep + "echo" + suffix): |
| return 0 |
| self.fail("unexpected: %s, %s" % (args, kwargs)) |
| return 1 |
| |
| self.popen_fakes.append(fake_ensure) |
| |
| self.capture_popen_env = True |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| |
| # The CIPD client was bootstrapped and hardlinked (or copied on Win). |
| client_binary_file = os.path.join( |
| cipd_cache, "clients", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
| ) |
| self.assertTrue(fs.isfile(client_binary_file)) |
| client_binary_link = os.path.join( |
| cipd_cache, "bin", "cipd" + cipd.EXECUTABLE_SUFFIX |
| ) |
| self.assertTrue(fs.isfile(client_binary_link)) |
| |
| env = self.popen_calls[1][1].pop("env") |
| exec_path = self.ir_dir("a", "bin", "echo") |
| if sys.platform == "win32": |
| exec_path += ".exe" |
| self.assertEqual( |
| [ |
| ( |
| [exec_path, "hello", "world"], |
| { |
| "cwd": self.ir_dir("a"), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": subprocess42.Containment(), |
| }, |
| ), |
| ], |
| # Ignore `cipd ensure` for isolated client here. |
| self.popen_calls[1:], |
| ) |
| |
| # Directory with cipd client is in front of PATH. |
| path = env["PATH"].split(os.pathsep) |
| self.assertEqual(os.path.join(cipd_cache, "bin"), path[0]) |
| |
| # CIPD_CACHE_DIR is set. |
| self.assertEqual(os.path.join(cipd_cache, "cache"), env["CIPD_CACHE_DIR"]) |
| |
| def test_main_relative_cwd_no_cmd(self): |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--relative-cwd", |
| "a", |
| ] |
| with self.assertRaises(SystemExit): |
| run_isolated.main(cmd) |
| |
| def test_main_bad_relative_cwd(self): |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--relative-cwd", |
| "a/../../b", |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| with self.assertRaises(SystemExit): |
| run_isolated.main(cmd) |
| |
| def test_main_naked_with_caches(self): |
| # An empty named cache is not kept! |
| # Interestingly, because we would need to put something in the named cache |
| # for it to be kept, we need the tool to write to it. This is tested in the |
| # smoke test. |
| trimmed = [] |
| |
| def trim_caches(caches, root, min_free_space, max_age_secs): |
| trimmed.append(True) |
| self.assertEqual(2, len(caches)) |
| self.assertTrue(root) |
| # The name cache root is increased by the sum of the two hints and buffer. |
| self.assertEqual( |
| run_isolated._FREE_SPACE_BUFFER_FOR_CIPD_PACKAGES |
| + 2 * 1024 * 1024 * 1024 |
| + 1100, |
| min_free_space, |
| ) |
| self.assertEqual(1814400, max_age_secs) |
| |
| self.mock(local_caching, "trim_caches", trim_caches) |
| nc = os.path.join(self.tempdir, "named_cache") |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--leak-temp-dir", |
| "100", |
| "--named-cache-root", |
| nc, |
| "--named-cache", |
| "cache_foo", |
| "foo", |
| "100", |
| "--named-cache", |
| "cache_bar", |
| "bar", |
| "1000", |
| "--", |
| "bin/echo${EXECUTABLE_SUFFIX}", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| |
| for cache_name in ("cache_foo", "cache_bar"): |
| named_path = os.path.join(nc, "named", cache_name) |
| self.assertFalse(fs.exists(named_path)) |
| self.assertTrue(trimmed) |
| |
| def test_main_clean(self): |
| cas_cache_dir = os.path.join(self.tempdir, "cas_cache") |
| named_cache_dir = os.path.join(self.tempdir, "named_cache") |
| kvs_dir = os.path.join(self.tempdir, "kvs_dir") |
| os.mkdir(kvs_dir) |
| with open(os.path.join(kvs_dir, "dummy"), "w") as f: |
| f.write("0" * 100) |
| |
| # override size threshold. |
| self.mock(run_isolated, "_CAS_KVS_CACHE_THRESHOLD", 99) |
| |
| min_free_space = 1 |
| max_cache_size = 2 |
| |
| cmd = [ |
| "--no-log", |
| "--clean", |
| # Shared options. |
| "--min-free-space", |
| str(min_free_space), |
| "--max-cache-size", |
| str(max_cache_size), |
| # CAS cache option. |
| "--cas-cache", |
| cas_cache_dir, |
| # Named cache option. |
| "--named-cache-root", |
| named_cache_dir, |
| "--kvs-dir", |
| kvs_dir, |
| ] |
| |
| def trim_caches_mock(caches, _root_dir, min_free_space, max_age_secs): |
| self.assertEqual(min_free_space, min_free_space) |
| self.assertEqual(max_age_secs, run_isolated.MAX_AGE_SECS) |
| |
| # CAS cache. |
| cas_cache = caches[0] |
| self.assertEqual( |
| cas_cache.state_file, os.path.join(cas_cache_dir, cas_cache.STATE_FILE) |
| ) |
| self.assertEqual(cas_cache.policies.max_cache_size, max_cache_size) |
| self.assertEqual(cas_cache.policies.min_free_space, min_free_space) |
| self.assertIsNone(cas_cache.policies.max_items) |
| |
| # Named cache. |
| named_cache = caches[1] |
| self.assertEqual( |
| named_cache.state_file, |
| os.path.join(named_cache_dir, named_cache.STATE_FILE), |
| ) |
| # max_cache_size, max_items, max_age_secs are hardcoded in |
| # run_isolated.process_named_cache_options(). |
| self.assertEqual(named_cache._policies.max_cache_size, 1024**4) |
| self.assertEqual(named_cache._policies.min_free_space, min_free_space) |
| self.assertEqual(named_cache._policies.max_items, 50) |
| self.assertEqual( |
| named_cache._policies.max_age_secs, run_isolated.MAX_AGE_SECS |
| ) |
| |
| self.mock(local_caching, "trim_caches", trim_caches_mock) |
| |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| |
| with self.assertRaises(OSError): |
| # kvs dir should be removed. |
| fs.stat(kvs_dir) |
| |
| def test_modified_cwd(self): |
| self._run_tha_test(command=["../out/some.exe", "arg"], relative_cwd="some") |
| self.assertEqual( |
| [ |
| ( |
| [self.ir_dir("out", "some.exe"), "arg"], |
| { |
| "cwd": self.ir_dir("some"), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": None, |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_run_tha_test_non_isolated(self): |
| _ = self._run_tha_test(command=["/bin/echo", "hello", "world"]) |
| self.assertEqual( |
| [ |
| ( |
| ["/bin/echo", "hello", "world"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": False, |
| "containment": None, |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_main_containment(self): |
| def fake_wait(args, **kwargs): # pylint: disable=unused-argument |
| # Success. |
| return 0 |
| |
| self.popen_fakes.append(fake_wait) |
| cmd = self.DISABLE_CIPD_FOR_TESTS + [ |
| "--no-log", |
| "--lower-priority", |
| "--containment-type", |
| "JOB_OBJECT", |
| "--limit-processes", |
| "42", |
| "--limit-total-committed-memory", |
| "1024", |
| "--root-dir", |
| self.tempdir, |
| "--", |
| "/bin/echo", |
| "hello", |
| "world", |
| ] |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| self.assertEqual( |
| [ |
| ( |
| ["/bin/echo", "hello", "world"], |
| { |
| "cwd": self.ir_dir(), |
| "detached": True, |
| "close_fds": True, |
| "lower_priority": True, |
| "containment": subprocess42.Containment( |
| containment_type=subprocess42.Containment.JOB_OBJECT, |
| limit_processes=42, |
| limit_total_committed_memory=1024, |
| ), |
| }, |
| ), |
| ], |
| self.popen_calls, |
| ) |
| |
| |
| class RunIsolatedTestRun(RunIsolatedTestBase): |
| def setUp(self): |
| super(RunIsolatedTestRun, self).setUp() |
| # Starts a full CAS server mock and have run_tha_test() uploads results |
| # back after the task completed. |
| self._server = cas_util.LocalCAS(self.tempdir) |
| self._server.start() |
| |
| def tearDown(self): |
| self._server.stop() |
| super(RunIsolatedTestRun, self).tearDown() |
| |
| # Runs the actual command requested. |
| def test_output(self): |
| digest = self._server.archive_files( |
| {"cmd.py": b'import sys\nopen(sys.argv[1], "w").write("bar")\n'} |
| ) |
| |
| os.environ["RUN_ISOLATED_CAS_ADDRESS"] = self._server.address |
| data = run_isolated.TaskData( |
| command=["python3", "cmd.py", "${ISOLATED_OUTDIR}/foo"], |
| relative_cwd=None, |
| cas_instance=None, |
| cas_digest=digest, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| |
| result_json = os.path.join(self.tempdir, "result.json") |
| ret = run_isolated.run_tha_test(data, result_json) |
| self.assertEqual(0, ret) |
| with open(result_json) as f: |
| result = json.load(f) |
| |
| output_digest = result["cas_output_root"]["digest"] |
| dest = os.path.join(self.tempdir, "output") |
| self._server.download( |
| output_digest["hash"] + "/" + str(output_digest["size_bytes"]), dest |
| ) |
| |
| self.assertEqual(os.listdir(dest), ["foo"]) |
| with open(os.path.join(dest, "foo"), "rb") as f: |
| self.assertEqual(f.read(), b"bar") |
| |
| def test_write_without_empty_cas_does_not_crash(self): |
| """Test is here to prevent regression of: https://crbug.com/1504567""" |
| cmd = """ |
| import os |
| outpath = os.path.join("${ISOLATED_OUTDIR}", "foo") |
| with open(outpath, "w") as f: |
| f.write("dont crash") |
| """ |
| os.environ.pop("RUN_ISOLATED_CAS_ADDRESS", None) |
| data = run_isolated.TaskData( |
| command=["python3", "-c", cmd], |
| relative_cwd=None, |
| cas_instance=None, |
| cas_digest=None, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| |
| result_json = os.path.join(self.tempdir, "result.json") |
| ret = run_isolated.run_tha_test(data, result_json) |
| self.assertEqual(0, ret) |
| |
| |
| class RunIsolatedTestCase(RunIsolatedTestRun): |
| def test_bad_cas_json_output(self): |
| def dump_bad_json(cmd, _): |
| json_path = cmd[cmd.index("-dump-json") + 1] |
| with open(json_path, "w") as fp: |
| fp.write("{i[]nv[[]alid js{}on") |
| |
| digest = self._server.archive_files( |
| {"cmd.py": b'import sys\nopen(sys.argv[1], "w").write("bar")\n'} |
| ) |
| |
| self.mock(run_isolated, "_run_go_cmd_and_wait", dump_bad_json) |
| |
| data = run_isolated.TaskData( |
| command=["python3", "cmd.py", "${ISOLATED_OUTDIR}/foo"], |
| relative_cwd=None, |
| cas_instance="some_instance", |
| cas_digest=digest, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs=None, |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| |
| result_json = os.path.join(self.tempdir, "result.json") |
| run_isolated.run_tha_test(data, result_json) |
| with open(result_json) as f: |
| result = json.load(f) |
| # Don't care the exact error, so long as its not reported as a missing_cas |
| self.assertTrue(result.get("internal_failure")) |
| self.assertIsNone(result.get("missing_cas")) |
| |
| def test_bad_cas_digest(self): |
| digest = "notvaliddigest" |
| os.environ["RUN_ISOLATED_CAS_ADDRESS"] = self._server.address |
| data = run_isolated.TaskData( |
| command=["python3", "cmd.py", "${ISOLATED_OUTDIR}/foo"], |
| relative_cwd=None, |
| cas_instance="some_instance", |
| cas_digest=digest, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| |
| result_json = os.path.join(self.tempdir, "result.json") |
| ret = run_isolated.run_tha_test(data, result_json) |
| with open(result_json) as f: |
| result = json.load(f) |
| self.assertEqual(1, ret) |
| self.assertEqual(digest, result["missing_cas"][0]["digest"]) |
| self.assertEqual("some_instance", result["missing_cas"][0]["instance"]) |
| |
| def test_bad_cipd_package(self): |
| def emulate_bad_cipd(_run_dir, cas_dir): |
| raise errors.NonRecoverableCipdException( |
| "missing_cipd", "foo_package", "not/found/foo", "deadbeef" |
| ) |
| |
| data = run_isolated.TaskData( |
| command=["python3", "cmd.py", "${ISOLATED_OUTDIR}/foo"], |
| relative_cwd=None, |
| cas_instance=None, |
| cas_digest=None, |
| outputs=None, |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=emulate_bad_cipd, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| |
| result_json = os.path.join(self.tempdir, "result.json") |
| ret = run_isolated.run_tha_test(data, result_json) |
| with open(result_json) as f: |
| result = json.load(f) |
| |
| self.assertEqual(1, ret) |
| self.assertEqual("foo_package", result["missing_cipd"][0]["package_name"]) |
| self.assertEqual("not/found/foo", result["missing_cipd"][0]["path"]) |
| self.assertEqual("deadbeef", result["missing_cipd"][0]["version"]) |
| |
| |
| FILE, LINK, RELATIVE_LINK, DIR = range(4) |
| |
| |
| class RunIsolatedTestOutputs(RunIsolatedTestBase): |
| # Unit test for link_outputs_to_outdir function. |
| |
| def create_src_tree(self, run_dir, src_dir): |
| # Create files and directories specified by src_dir in run_dir. |
| for path in src_dir: |
| full_path = os.path.join(run_dir, path) |
| (t, content) = src_dir[path] |
| if t == FILE: |
| with open(full_path, "w") as f: |
| f.write(content) |
| elif t == RELATIVE_LINK: |
| fs.symlink(content, full_path) |
| elif t == LINK: |
| root_dir = os.path.join(self.tempdir, "ir") |
| real_path = os.path.join(root_dir, content) |
| fs.symlink(real_path, full_path) |
| else: |
| fs.mkdir(full_path) |
| self.create_src_tree(os.path.join(run_dir, path), content) |
| |
| def link_outputs_test(self, src_dir, outputs): |
| run_dir = os.path.join(self.tempdir, "ir") |
| out_dir = os.path.join(self.tempdir, "io") |
| fs.mkdir(run_dir) |
| fs.mkdir(out_dir) |
| self.create_src_tree(run_dir, src_dir) |
| run_isolated.link_outputs_to_outdir(run_dir, out_dir, outputs) |
| |
| def test_file(self): |
| src_dir = { |
| "foo_file": (FILE, "contents of foo"), |
| } |
| outputs = ["foo_file"] |
| expected = { |
| "foo_file": "contents of foo", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_file(self): |
| src_dir = { |
| "foo_file": (FILE, "contents of foo"), |
| "foo_link": (LINK, "foo_file"), |
| } |
| outputs = ["foo_link"] |
| expected = { |
| "foo_link": "contents of foo", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_dir_containing_files(self): |
| src_dir = { |
| "subdir": ( |
| DIR, |
| { |
| "child_a": (FILE, "contents of a"), |
| "child_b": (FILE, "contents of b"), |
| }, |
| ) |
| } |
| outputs = [os.path.join("subdir", "")] |
| expected = { |
| os.path.join("subdir", "child_a"): "contents of a", |
| os.path.join("subdir", "child_b"): "contents of b", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_relative_symlink(self): |
| src_dir = { |
| "foo_file": (FILE, "contents of foo"), |
| "subdir": ( |
| DIR, |
| { |
| "foo_link": (RELATIVE_LINK, "../foo_file"), |
| "subsubdir": ( |
| DIR, |
| { |
| "bar_link": (RELATIVE_LINK, "../foo_link"), |
| }, |
| ), |
| }, |
| ), |
| } |
| outputs = [os.path.join("subdir", "subsubdir", "bar_link")] |
| expected = { |
| os.path.join("subdir", "subsubdir", "bar_link"): "contents of foo", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_dir_containing_files(self): |
| src_dir = { |
| "subdir_link": (LINK, "subdir"), |
| "subdir": ( |
| DIR, |
| { |
| "child_a": (FILE, "contents of a"), |
| }, |
| ), |
| } |
| outputs = ["subdir_link"] |
| expected = { |
| os.path.join("subdir_link", "child_a"): "contents of a", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_symlink_to_dir_containing_files(self): |
| src_dir = { |
| "subdir_link": (LINK, "subdir_link2"), |
| "subdir_link2": (LINK, "subdir"), |
| "subdir": ( |
| DIR, |
| { |
| "child_a": (FILE, "contents of a"), |
| "child_b": (FILE, "contents of b"), |
| }, |
| ), |
| } |
| outputs = ["subdir_link"] |
| expected = { |
| os.path.join("subdir_link", "child_a"): "contents of a", |
| os.path.join("subdir_link", "child_b"): "contents of b", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_empty_dir(self): |
| src_dir = { |
| "subdir": (DIR, {}), |
| } |
| outputs = [os.path.join("subdir", "")] |
| expected = { |
| os.path.join("subdir", ""): "", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_dir_ignore_trailing_slash(self): |
| src_dir = { |
| "subdir": (DIR, {}), |
| } |
| outputs = [os.path.join("subdir", "")] |
| expected = { |
| "subdir": "", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_dir_containing_empty_dir(self): |
| src_dir = { |
| "subdir": ( |
| DIR, |
| { |
| "subsubdir": (DIR, ""), |
| }, |
| ), |
| } |
| outputs = [os.path.join("subdir", "")] |
| expected = { |
| os.path.join("subdir", "subsubdir", ""): "", |
| os.path.join("subdir", "subsubdir", ""): "", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_empty_dir(self): |
| src_dir = { |
| "subdir": (DIR, {}), |
| "subdir_link": (LINK, "subdir"), |
| } |
| outputs = ["subdir_link"] |
| expected = { |
| os.path.join("subdir_link", ""): "", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_nonexistent_file(self): |
| src_dir = { |
| "bad_link": (LINK, "nonexistent_file"), |
| } |
| outputs = ["bad_link"] |
| expected = {} |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_symlink_to_file(self): |
| src_dir = { |
| "first_link": (LINK, "second_link"), |
| "second_link": (LINK, "foo_file"), |
| "foo_file": (FILE, "contents of foo"), |
| } |
| outputs = ["first_link"] |
| expected = { |
| "first_link": "contents of foo", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_symlink_to_nonexistent_file(self): |
| src_dir = { |
| "first_link": (LINK, "second_link"), |
| "second_link": (LINK, "nonexistent_file"), |
| } |
| outputs = ["first_link"] |
| expected = {} |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_file_in_parent(self): |
| src_dir = { |
| "subdir": ( |
| DIR, |
| { |
| "subsubdir": ( |
| DIR, |
| { |
| "foo_link": (LINK, "subdir/foo_file"), |
| }, |
| ), |
| "foo_file": (FILE, "contents of foo"), |
| }, |
| ), |
| } |
| outputs = [ |
| os.path.join("subdir", "subsubdir", "foo_link"), |
| os.path.join("subdir", "foo_file"), |
| ] |
| expected = { |
| os.path.join("subdir", "subsubdir", "foo_link"): "contents of foo", |
| os.path.join("subdir", "foo_file"): "contents of foo", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_file_in_dir(self): |
| src_dir = { |
| "subdir_link": (LINK, "subdir/child_a"), |
| "subdir": ( |
| DIR, |
| { |
| "child_a": (FILE, "contents of a"), |
| }, |
| ), |
| } |
| outputs = ["subdir_link"] |
| expected = { |
| "subdir_link": "contents of a", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| def test_symlink_to_symlink_to_file_in_dir(self): |
| src_dir = { |
| "first_link": (LINK, "subdir_link"), |
| "subdir_link": (LINK, "subdir/child_a"), |
| "subdir": ( |
| DIR, |
| { |
| "child_a": (FILE, "contents of a"), |
| }, |
| ), |
| } |
| outputs = ["first_link"] |
| expected = { |
| "first_link": "contents of a", |
| } |
| self.link_outputs_test(src_dir, outputs) |
| self.assertExpectedTree(expected) |
| |
| |
| class RunIsolatedTestOutputFiles(RunIsolatedTestBase): |
| def setUp(self): |
| super(RunIsolatedTestOutputFiles, self).setUp() |
| |
| # Starts a full CAS server mock and have run_tha_test() uploads results |
| # back after the task completed. |
| self._server = cas_util.LocalCAS(self.tempdir) |
| self._server.start() |
| os.environ["RUN_ISOLATED_CAS_ADDRESS"] = self._server.address |
| |
| def tearDown(self): |
| self._server.stop() |
| super(RunIsolatedTestOutputFiles, self).tearDown() |
| |
| # Like RunIsolatedTestRun, but ensures that specific output files |
| # (as opposed to anything in $(ISOLATED_OUTDIR)) are returned. |
| def test_output_cmd(self): |
| # Output the following structure: |
| # |
| # foo1 |
| # foodir --> foo2_sl (symlink to "foo2_content" file) |
| # bardir --> bar1 |
| # |
| # Create the symlinks only on Linux. |
| cas_digest = self._server.archive_files( |
| { |
| "cmd.py": b"import os\n" |
| b"import sys\n" |
| b'open(sys.argv[1], "w").write("foo1")\n' |
| b'bar1_path = os.path.join(sys.argv[3], "bar1")\n' |
| b'open(bar1_path, "w").write("bar1")\n' |
| b'if sys.platform == "linux":\n' |
| b' foo_realpath = os.path.abspath("foo2_content")\n' |
| b' open(foo_realpath, "w").write("foo2")\n' |
| b" os.symlink(foo_realpath, sys.argv[2])\n" |
| b"else:\n" |
| b' open(sys.argv[2], "w").write("foo2")\n' |
| } |
| ) |
| |
| data = run_isolated.TaskData( |
| command=["python3", "cmd.py", "foo1", "foodir/foo2_sl", "bardir/"], |
| relative_cwd=None, |
| cas_instance=None, |
| cas_digest=cas_digest, |
| outputs=[ |
| "foo1", |
| # They must be in OS native path. |
| os.path.join("foodir", "foo2_sl"), |
| os.path.join("bardir", ""), |
| ], |
| install_named_caches=init_named_caches_stub, |
| leak_temp_dir=False, |
| root_dir=self.tempdir, |
| hard_timeout=60, |
| grace_period=30, |
| bot_file=None, |
| switch_to_account=False, |
| install_packages_fn=run_isolated.copy_local_packages, |
| cas_cache_dir="", |
| cas_cache_policies=local_caching.CachePolicies(0, 0, 0, 0), |
| cas_kvs="", |
| env={}, |
| env_prefix={}, |
| lower_priority=False, |
| containment=None, |
| trim_caches_fn=trim_caches_stub, |
| ) |
| result_json = os.path.join(self.tempdir, "result.json") |
| ret = run_isolated.run_tha_test(data, result_json) |
| self.assertEqual(0, ret) |
| |
| # It uploaded back. Assert the store has a new item containing foo. |
| |
| with open(result_json) as f: |
| result = json.load(f) |
| |
| output_digest = result["cas_output_root"]["digest"] |
| dest = os.path.join(self.tempdir, "outputs") |
| self._server.download( |
| output_digest["hash"] + "/" + str(output_digest["size_bytes"]), dest |
| ) |
| |
| self.assertExpectedTree( |
| { |
| os.path.join("foodir", "foo2_sl"): "foo2", |
| "foo1": "foo1", |
| os.path.join("bardir", "bar1"): "bar1", |
| }, |
| dest, |
| ) |
| |
| self.assertCountEqual(os.listdir(dest), ["foodir", "foo1", "bardir"]) |
| self.assertEqual(os.listdir(os.path.join(dest, "foodir")), ["foo2_sl"]) |
| self.assertEqual(os.listdir(os.path.join(dest, "bardir")), ["bar1"]) |
| |
| |
| class RunIsolatedJsonTest(RunIsolatedTestBase): |
| # Similar to RunIsolatedTest but adds the hacks to process ISOLATED_OUTDIR to |
| # generate a json result file. |
| |
| def setUp(self): |
| super(RunIsolatedJsonTest, self).setUp() |
| |
| # Starts a full CAS server mock and have run_tha_test() uploads results |
| # back after the task completed. |
| self._server = cas_util.LocalCAS(self.tempdir) |
| self._server.start() |
| os.environ["RUN_ISOLATED_CAS_ADDRESS"] = self._server.address |
| |
| def tearDown(self): |
| self._server.stop() |
| super(RunIsolatedJsonTest, self).tearDown() |
| |
| def test_main_json(self): |
| sub_cmd = [ |
| "python3", |
| "-c", |
| 'import sys; open(sys.argv[1], "wb").write(b"generated data\\n")', |
| "${ISOLATED_OUTDIR}/out.txt", |
| ] |
| |
| out = os.path.join(self.tempdir, "res.json") |
| cmd = ( |
| self.DISABLE_CIPD_FOR_TESTS |
| + [ |
| "--no-log", |
| "--named-cache-root", |
| os.path.join(self.tempdir, "named_cache"), |
| "--json", |
| out, |
| "--root-dir", |
| self.tempdir, |
| "--", |
| ] |
| + sub_cmd |
| ) |
| ret = run_isolated.main(cmd) |
| self.assertEqual(0, ret) |
| |
| cas_hash = ( |
| "5868195adddf130eeb09a939bc40a17033fb058288b7b0bec0144dcb07e9cf78" |
| ) |
| |
| expected = { |
| "exit_code": 0, |
| "had_hard_timeout": False, |
| "internal_failure": None, |
| "cas_output_root": { |
| # Local cas server uses its own default instance. |
| "cas_instance": None, |
| "digest": {"hash": cas_hash, "size_bytes": 81}, |
| }, |
| "outputs_ref": None, |
| "stats": { |
| "trim_caches": {}, |
| "isolated": { |
| "download": {}, |
| "upload": { |
| "items_cold": [15, 81], |
| "items_hot": None, |
| "result": "success", |
| "size_cold": 96, |
| "size_hot": 0, |
| }, |
| }, |
| "named_caches": { |
| "install": {}, |
| "uninstall": {}, |
| }, |
| "cleanup": {}, |
| }, |
| "version": 5, |
| } |
| actual = tools.read_json(out) |
| # duration can be exactly 0 due to low timer resolution, especially but not |
| # exclusively on Windows. |
| self.assertLessEqual(0, actual.pop("duration")) |
| self.assertLessEqual(0, actual["stats"]["trim_caches"].pop("duration")) |
| actual_upload_stats = actual["stats"]["isolated"]["upload"] |
| self.assertLessEqual(0, actual_upload_stats.pop("duration")) |
| named_caches_stats = actual["stats"]["named_caches"] |
| self.assertLessEqual(0, named_caches_stats["install"].pop("duration")) |
| self.assertLessEqual(0, named_caches_stats["uninstall"].pop("duration")) |
| self.assertLessEqual(0, actual["stats"]["cleanup"].pop("duration")) |
| for i in ("items_cold", "items_hot"): |
| if actual_upload_stats[i]: |
| actual_upload_stats[i] = large.unpack( |
| base64.b64decode(actual_upload_stats[i]) |
| ) |
| self.assertEqual(expected, actual) |
| |
| |
| if __name__ == "__main__": |
| test_env.main() |