blob: e462f27fc588c74e0728cbc954efaa0d43346e7f [file] [edit]
#!/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()