blob: fd4d68e8b07d85eaae4ed1f7fdcc5930c8e30c93 [file] [edit]
#!/usr/bin/env vpython3
# Copyright 2018 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.
import hashlib
import json
import os
import random
import string
import subprocess
import sys
import tempfile
import time
import unittest
# Mutates sys.path.
import test_env
# third_party/
from depot_tools import auto_stub
import local_caching
from utils import file_path
from utils import fs
from utils import lru
def write_file(path, contents):
with fs.open(path, "wb") as f:
f.write(contents)
def read_file(path):
with fs.open(path, "rb") as f:
return f.read()
def read_tree(path):
"""Returns a dict with {filepath: content}."""
if not fs.isdir(path):
return None
out = {}
for root, _, filenames in fs.walk(path):
for filename in filenames:
p = os.path.join(root, filename)
out[os.path.relpath(p, path)] = read_file(p)
return out
def _gen_data(size):
return (string.digits.encode() * ((size + 9) // 10))[:size]
class TestCase(auto_stub.TestCase):
def setUp(self):
super(TestCase, self).setUp()
self.tempdir = tempfile.mkdtemp(prefix="local_caching")
self._algo = hashlib.sha256
# Time mocking.
self._now = 1000
self.mock(time, "time", lambda: float(self._now))
# Free disk space mocking.
self._free_disk = 1000
self.mock(file_path, "get_free_space", lambda _: self._free_disk)
# Named cache works with directories.
def rmtree(p):
self._free_disk += file_path.get_recursive_size(p)
return old_rmtree(p)
old_rmtree = self.mock(file_path, "rmtree", rmtree)
# Isolated cache works with files.
def try_remove(p):
try:
self._free_disk += fs.stat(p).st_size
except OSError:
pass
return old_try_remove(p)
old_try_remove = self.mock(file_path, "try_remove", try_remove)
def tearDown(self):
try:
file_path.rmtree(self.tempdir)
finally:
super(TestCase, self).tearDown()
def _add_one_item(self, cache, size):
"""Adds one item of |size| bytes in the cache and returns the created name."""
# Don't allow 0 byte items here. This doesn't work for named cache.
self.assertTrue(size)
data = _gen_data(size)
if isinstance(cache, local_caching.ContentAddressedCache):
# This covers both MemoryContentAddressedCache and
# DiskContentAddressedCache.
return cache.write(self._algo(data).hexdigest(), [data])
if isinstance(cache, local_caching.NamedCache):
# In this case, map a named cache, add a file, unmap it.
dest_dir = os.path.join(self.tempdir, "dest")
self.assertFalse(fs.exists(dest_dir))
name = str(size)
cache.install(dest_dir, name)
# Put a file in there named 'hello', otherwise it'll stay empty.
with fs.open(os.path.join(dest_dir, "hello"), "wb") as f:
f.write(data)
cache.uninstall(dest_dir, name)
self.assertFalse(fs.exists(dest_dir))
return name
self.fail("Unexpected cache type %r" % cache)
return None
def _get_policies(
max_cache_size=0, min_free_space=0, max_items=0, max_age_secs=0
):
"""Returns a CachePolicies with only the policy we want to enforce."""
return local_caching.CachePolicies(
max_cache_size=max_cache_size,
min_free_space=min_free_space,
max_items=max_items,
max_age_secs=max_age_secs,
)
class CacheTestMixin:
"""Adds testing for the Cache interface."""
# pylint: disable=no-member
def get_cache(self, policies):
raise NotImplementedError()
def test_contains(self):
cache = self.get_cache(_get_policies())
self.assertFalse("foo" in cache)
name = self._add_one_item(cache, 1)
self.assertFalse("foo" in cache)
self.assertTrue(name in cache)
def test_iter(self):
cache = self.get_cache(_get_policies())
self.assertEqual([], list(cache))
n10 = self._add_one_item(cache, 10)
n3 = self._add_one_item(cache, 3)
self.assertEqual([n10, n3], list(cache))
def test_len(self):
cache = self.get_cache(_get_policies())
self.assertEqual(0, len(cache))
self._add_one_item(cache, 10)
self.assertEqual(1, len(cache))
self._add_one_item(cache, 11)
self.assertEqual(2, len(cache))
def test_total_size(self):
cache = self.get_cache(_get_policies())
self.assertEqual(0, cache.total_size)
self._add_one_item(cache, 10)
self.assertEqual(10, cache.total_size)
self._add_one_item(cache, 11)
self.assertEqual(21, cache.total_size)
def test_added(self):
cache = self.get_cache(_get_policies())
self.assertEqual([], cache.added)
self._add_one_item(cache, 10)
self.assertEqual([10], cache.added)
def test_used(self):
# It depends on the implementation.
pass
def test_oldest_evictable_item(self):
cache = self.get_cache(_get_policies())
self.assertEqual(None, cache.oldest_evictable_ts())
ts = self._now
self._add_one_item(cache, 10)
self._now += 10
self._add_one_item(cache, 20)
self.assertEqual(ts, cache.oldest_evictable_ts())
def test_remove_oldest(self):
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 10)
self._add_one_item(cache, 100)
self.assertTrue(cache.remove_oldest_evictable_item())
self._add_one_item(cache, 20)
# added is not yet updated.
self.assertEqual([10, 100, 20], cache.added)
def test_save(self):
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 100)
self._add_one_item(cache, 101)
# Just assert it doesn't crash here.
cache.save()
def test_trim(self):
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 100)
self._add_one_item(cache, 101)
self.assertEqual([], cache.trim())
def test_true(self):
cache = self.get_cache(_get_policies())
self.assertEqual(0, len(cache))
# Even if empty, it's still "True".
self.assertTrue(bool(cache))
class ContentAddressedCacheTestMixin(CacheTestMixin):
"""Add testing for the ContentAddressedCache interface."""
# pylint: disable=abstract-method
_now = 0
# write() is indirectly tested with _add_one_item().
# cleanup() requires specific setup.
def test_touch(self):
cache = self.get_cache(_get_policies())
self._now = 1000
n1 = self._add_one_item(cache, 1)
self._now = 1001
n2 = self._add_one_item(cache, 2)
self.assertEqual([n1, n2], list(cache))
self.assertEqual(1000, cache.oldest_evictable_ts())
self._now = 1002
cache.touch(n1, None)
self.assertEqual([n2, n1], list(cache))
self.assertEqual(1001, cache.oldest_evictable_ts())
def test_getfileobj(self):
cache = self.get_cache(_get_policies())
h = self._add_one_item(cache, 1)
with cache.getfileobj(h) as f:
self.assertEqual(b"0", f.read())
def test_getfileobj_complete_miss(self):
cache = self.get_cache(_get_policies())
with self.assertRaises(local_caching.CacheMiss):
cache.getfileobj("0" * 40)
def test_getfileobj_cache_state_missing(self):
# Put the file in the cache, but do NOT save cache state.
cache = self.get_cache(_get_policies())
h = self._add_one_item(cache, 1)
# Since we didn't save the state, this should result in CacheMiss.
cache = self.get_cache(_get_policies())
with self.assertRaises(local_caching.CacheMiss):
cache.getfileobj(h)
class MemoryContentAddressedCacheTest(TestCase, ContentAddressedCacheTestMixin):
def get_cache(self, policies):
return local_caching.MemoryContentAddressedCache(policies)
def cleanup(self):
# Doesn't do anything.
cache = self.get_cache(_get_policies())
cache.cleanup()
class DiskContentAddressedCacheTest(TestCase, ContentAddressedCacheTestMixin):
def setUp(self):
super(DiskContentAddressedCacheTest, self).setUp()
# If this fails on Windows, please rerun this tests as an elevated user with
# administrator access right.
self.assertEqual(True, file_path.enable_symlink())
def cache_dir(self):
return os.path.join(self.tempdir, "cache")
def get_cache(self, policies):
return local_caching.DiskContentAddressedCache(
self.cache_dir(), policies, trim=True
)
def test_write_policies_free_disk(self):
cache = self.get_cache(_get_policies(min_free_space=1000))
with self.assertRaises(local_caching.NoMoreSpace):
self._add_one_item(cache, 1)
def test_write_policies_fit(self):
self._free_disk = 1001
cache = self.get_cache(_get_policies(min_free_space=1000))
with self.assertRaises(local_caching.NoMoreSpace):
self._add_one_item(cache, 2)
def test_write_policies_max_cache_size(self):
# max_cache_size is ignored while adding items.
cache = self.get_cache(_get_policies(max_cache_size=1))
self._add_one_item(cache, 2)
self._add_one_item(cache, 3)
def test_write_policies_max_items(self):
# max_items is ignored while adding items.
cache = self.get_cache(_get_policies(max_items=1))
self._add_one_item(cache, 2)
self._add_one_item(cache, 3)
def test_write_policies_min_free_space(self):
# min_free_space is enforced while adding items.
self._free_disk = 1005
cache = self.get_cache(_get_policies(min_free_space=1000))
self._add_one_item(cache, 2)
self._add_one_item(cache, 3)
# Mapping more content than the amount of free disk required.
with self.assertRaises(local_caching.NoMoreSpace) as cm:
self._add_one_item(cache, 1)
expected = (
"Not enough space to fetch the whole isolated tree.\n "
"CachePolicies(max_cache_size=0 (0.000 GiB); max_items=0; "
"min_free_space=1000 (0.000 GiB); max_age_secs=0)\n "
"cache=6 bytes (0.000 GiB), 3 items; "
"999 bytes (0.000 GiB) free_space"
)
self.assertEqual(expected, str(cm.exception))
def test_save_disk(self):
cache = self.get_cache(_get_policies())
self.assertEqual(
sorted([cache.STATE_FILE]), sorted(fs.listdir(cache.cache_dir))
)
h = self._add_one_item(cache, 2)
self.assertEqual(
sorted([h, cache.STATE_FILE]), sorted(fs.listdir(cache.cache_dir))
)
items = lru.LRUDict.load(os.path.join(cache.cache_dir, cache.STATE_FILE))
self.assertEqual(0, len(items))
cache.save()
self.assertEqual(
sorted([h, cache.STATE_FILE]), sorted(fs.listdir(cache.cache_dir))
)
items = lru.LRUDict.load(os.path.join(cache.cache_dir, cache.STATE_FILE))
self.assertEqual(1, len(items))
self.assertEqual((h, [2, 1000]), items.get_oldest())
def test_cleanup_disk(self):
# Inject an item without a state.json, one is lost. Both will be deleted on
# cleanup.
self._free_disk = 1003
cache = self.get_cache(_get_policies(min_free_space=1000))
h_foo = self._algo(b"foo").hexdigest()
self.assertEqual([], sorted(cache._lru._items.items()))
cache.write(h_foo, [b"foo"])
self.assertEqual([], cache.trim())
self.assertEqual([h_foo], [i[0] for i in cache._lru._items.items()])
h_a = self._algo(b"a").hexdigest()
local_caching.file_write(os.path.join(cache.cache_dir, h_a), [b"a"])
# file_path.remove() explicitly handle the +R bit on Windows.
file_path.remove(os.path.join(cache.cache_dir, h_foo))
# Still hasn't realized that the file is missing.
self.assertEqual([h_foo], [i[0] for i in cache._lru._items.items()])
self.assertEqual(
sorted([h_a, cache.STATE_FILE]), sorted(fs.listdir(cache.cache_dir))
)
cache.cleanup()
self.assertEqual([cache.STATE_FILE], fs.listdir(cache.cache_dir))
def test_cleanup_disk_evict_corrupted_files(self):
self._free_disk = 1003
cache = self.get_cache(_get_policies(min_free_space=1000))
# add a corrupted item
h_a = self._algo(b"a").hexdigest()
cache.write(h_a, [b"A"])
h_b = self._algo(b"b").hexdigest()
cache.write(h_b, [b"b"])
mtime_a = self._now
mtime_b = self._now
def _get_mtime(h):
if h == h_a:
return mtime_a
if h == h_b:
return mtime_b
self.mock(cache, "_get_mtime", _get_mtime)
self.assertEqual(
[(h_a, (1, mtime_a)), (h_b, (1, mtime_b))],
list(cache._lru._items.items()),
)
self.assertCountEqual(
([h_a, h_b, cache.STATE_FILE]), (fs.listdir(cache.cache_dir))
)
# if the mtime is same with the timestamp in state.json,
# the varification won't run.
cache.cleanup()
self.assertCountEqual(
([h_a, h_b, cache.STATE_FILE]), (fs.listdir(cache.cache_dir))
)
# if the mtime is after the timestamp in the state.json
# the varification will run and removed the corrupted file.
mtime_a += 1
mtime_b += 1
self.mock(cache._lru, "time_fn", lambda: mtime_b)
cache.cleanup()
self.assertCountEqual(
([h_b, cache.STATE_FILE]), (fs.listdir(cache.cache_dir))
)
self.assertCountEqual([(h_b, (1, mtime_b))], cache._lru._items.items())
def test_policies_active_trimming(self):
# Start with a larger cache, add many object.
# Reload the cache with smaller policies, the cache should be trimmed on
# load.
h_a = self._algo(b"a").hexdigest()
h_b = self._algo(b"b").hexdigest()
h_c = self._algo(b"c").hexdigest()
large = b"b" * 99
h_large = self._algo(large).hexdigest()
def assertItems(expected):
actual = [
(digest, size) for digest, (size, _) in cache._lru._items.items()
]
self.assertEqual(expected, actual)
self._free_disk = 1101
cache = self.get_cache(
_get_policies(max_cache_size=100, max_items=2, min_free_space=1000)
)
cache.write(h_a, [b"a"])
cache.write(h_large, [large])
# Cache (size and # items) is not enforced while adding items. The
# rationale is that a task may request more data than the size of the
# cache policies. As long as there is free space, this is fine.
cache.write(h_b, [b"b"])
assertItems([(h_a, 1), (h_large, len(large)), (h_b, 1)])
self.assertEqual(h_a, cache._protected)
self.assertEqual(1000, cache._free_disk)
# Free disk is enforced, because otherwise we assume the task wouldn't
# be able to start. In this case, it throws an exception since all items
# are protected. The item is added since it's detected after the fact.
with self.assertRaises(local_caching.NoMoreSpace):
cache.write(h_c, [b"c"])
self.assertEqual([1, 99], cache.trim())
# At this point, after the implicit trim in __exit__(), h_a and h_large were
# evicted.
self.assertEqual(
sorted([h_b, h_c, cache.STATE_FILE]), sorted(fs.listdir(cache.cache_dir))
)
# Allow 3 items and 101 bytes so h_large is kept.
cache = self.get_cache(
_get_policies(
max_cache_size=101, min_free_space=1000, max_items=3, max_age_secs=0
)
)
cache.write(h_large, [large])
self.assertEqual(3, len(cache))
self.assertEqual(101, cache.total_size)
self.assertEqual([], cache.trim())
self.assertEqual(
sorted([h_b, h_c, h_large, cache.STATE_FILE]),
sorted(fs.listdir(cache.cache_dir)),
)
# Assert that trimming is done in constructor too.
cache = self.get_cache(
_get_policies(
max_cache_size=100, min_free_space=1000, max_items=2, max_age_secs=0
)
)
assertItems([(h_c, 1), (h_large, len(large))])
self.assertEqual(None, cache._protected)
self.assertEqual(1202, cache._free_disk)
self.assertEqual(2, len(cache))
self.assertEqual(100, cache.total_size)
self.assertEqual([], cache.trim())
def test_trim_policies_trim_old(self):
# Add two items, one 3 weeks and one minute old, one recent, make sure the
# old one is trimmed.
cache = self.get_cache(
_get_policies(
max_cache_size=1000,
min_free_space=0,
max_items=1000,
max_age_secs=21 * 24 * 60 * 60,
)
)
self._now = 100
# Test the very limit of 3 weeks:
cache.write(self._algo(b"old").hexdigest(), [b"old"])
self._now += 1
cache.write(self._algo(b"recent").hexdigest(), [b"recent"])
self._now += 21 * 24 * 60 * 60
self.assertEqual([3], cache.trim())
self.assertEqual([self._algo(b"recent").hexdigest()], list(cache))
def test_some_file_brutally_deleted(self):
h_a = self._algo(b"a").hexdigest()
self._free_disk = 1100
cache = self.get_cache(_get_policies())
cache.write(h_a, [b"a"])
self.assertTrue(cache.touch(h_a, local_caching.UNKNOWN_FILE_SIZE))
self.assertTrue(cache.touch(h_a, 1))
self.assertEqual([], cache.trim())
# file_path.remove() explicitly handle the +R bit on Windows.
file_path.remove(os.path.join(cache.cache_dir, h_a))
cache = self.get_cache(_get_policies())
# 'Ghost' entry loaded with state.json is still there.
self.assertEqual([h_a], list(cache))
# 'touch' detects the file is missing by returning False.
self.assertFalse(cache.touch(h_a, local_caching.UNKNOWN_FILE_SIZE))
self.assertFalse(cache.touch(h_a, 1))
# 'touch' evicted the entry.
self.assertEqual([], list(cache))
def test_invalid_state(self):
file_path.ensure_tree(self.cache_dir())
statefile = os.path.join(
self.cache_dir(), local_caching.DiskContentAddressedCache.STATE_FILE
)
with open(statefile, "w") as f:
f.write("invalid")
with open(os.path.join(self.cache_dir(), "invalid"), "w") as f:
f.write("invalid")
_ = self.get_cache(_get_policies())
self.assertEqual(fs.listdir(self.cache_dir()), ["state.json"])
class NamedCacheTest(TestCase, CacheTestMixin):
def setUp(self):
super(NamedCacheTest, self).setUp()
self.cache_dir = os.path.join(self.tempdir, "cache")
def get_cache(self, policies):
return local_caching.NamedCache(self.cache_dir, policies)
def get_named_cache(self, policies, keep):
return local_caching.NamedCache(self.cache_dir, policies, keep=keep)
def test_clean_cache(self):
dest_dir = os.path.join(self.tempdir, "dest")
cache = self.get_cache(_get_policies())
self.assertEqual([], fs.listdir(cache.cache_dir))
a_path = os.path.join(dest_dir, "a")
b_path = os.path.join(dest_dir, "b")
self.assertEqual(0, cache.install(a_path, "1"))
self.assertEqual(0, cache.install(b_path, "2"))
self.assertEqual(
False, fs.exists(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
self.assertEqual({"a", "b"}, set(fs.listdir(dest_dir)))
self.assertFalse(cache.available)
self.assertEqual([cache.STATE_FILE], fs.listdir(cache.cache_dir))
write_file(os.path.join(a_path, "x"), b"x")
write_file(os.path.join(b_path, "y"), b"y")
self.assertEqual(1, cache.uninstall(a_path, "1"))
self.assertEqual(1, cache.uninstall(b_path, "2"))
self.assertEqual(4, len(fs.listdir(cache.cache_dir)))
path1 = os.path.join(cache.cache_dir, cache._lru["1"][0])
self.assertEqual(b"x", read_file(os.path.join(path1, "x")))
path2 = os.path.join(cache.cache_dir, cache._lru["2"][0])
self.assertEqual(b"y", read_file(os.path.join(path2, "y")))
self.assertEqual(
os.path.join("..", cache._lru["1"][0]),
fs.readlink(cache._get_named_path("1")),
)
self.assertEqual(
os.path.join("..", cache._lru["2"][0]),
fs.readlink(cache._get_named_path("2")),
)
self.assertEqual(
["1", "2"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_existing_cache(self):
# Ensures that the code does what is expected under number use.
dest_dir = os.path.join(self.tempdir, "dest")
cache = self.get_cache(_get_policies())
# Assume test_clean passes.
a_path = os.path.join(dest_dir, "a")
b_path = os.path.join(dest_dir, "b")
self.assertEqual(0, cache.install(a_path, "1"))
write_file(os.path.join(dest_dir, "a", "x"), b"x")
self.assertEqual(1, cache.uninstall(a_path, "1"))
# Test starts here.
self.assertEqual(1, cache.install(a_path, "1"))
self.assertEqual(0, cache.install(b_path, "2"))
self.assertEqual({"a", "b"}, set(fs.listdir(dest_dir)))
self.assertFalse(cache.available)
self.assertEqual(
sorted([cache.NAMED_DIR, cache.STATE_FILE]),
sorted(fs.listdir(cache.cache_dir)),
)
self.assertEqual(
[], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
self.assertEqual(b"x", read_file(os.path.join(dest_dir, "a", "x")))
write_file(os.path.join(a_path, "x"), b"x2")
write_file(os.path.join(b_path, "y"), b"y")
self.assertEqual(2, cache.uninstall(a_path, "1"))
self.assertEqual(1, cache.uninstall(b_path, "2"))
self.assertEqual(4, len(fs.listdir(cache.cache_dir)))
path1 = os.path.join(cache.cache_dir, cache._lru["1"][0])
self.assertEqual(b"x2", read_file(os.path.join(path1, "x")))
path2 = os.path.join(cache.cache_dir, cache._lru["2"][0])
self.assertEqual(b"y", read_file(os.path.join(path2, "y")))
self.assertEqual(
os.path.join("..", cache._lru["1"][0]),
fs.readlink(cache._get_named_path("1")),
)
self.assertEqual(
os.path.join("..", cache._lru["2"][0]),
fs.readlink(cache._get_named_path("2")),
)
self.assertEqual(
["1", "2"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_install_throws(self):
old_isdir = None
banged = []
# Crashes, but only on the first call.
def bang(path):
if not banged:
banged.append(True)
raise IOError("fake")
return old_isdir(path)
cache = self.get_cache(_get_policies())
dest_dir = os.path.join(self.tempdir, "dest")
# fs.isdir() happens to be the first function called.
old_isdir = self.mock(fs, "isdir", bang)
with self.assertRaises(local_caching.NamedCacheError):
cache.install(dest_dir, "1")
def test_uninstall_throws(self):
old_isdir = None
banged = []
# Crashes, but only on the first call.
def bang(path):
if not banged:
banged.append(True)
raise IOError("fake")
return old_isdir(path)
cache = self.get_cache(_get_policies())
dest_dir = os.path.join(self.tempdir, "dest")
self.assertEqual(0, cache.install(dest_dir, "1"))
# fs.isdir() happens to be the first function called.
old_isdir = self.mock(fs, "isdir", bang)
with self.assertRaises(local_caching.NamedCacheError):
cache.uninstall(dest_dir, "1")
def test_cycle_twice(self):
# Ensure that named symlink works.
cache = self.get_cache(_get_policies())
dest_dir = os.path.join(self.tempdir, "dest")
self.assertEqual(0, cache.install(dest_dir, "1"))
with fs.open(os.path.join(dest_dir, "hi"), "wb") as f:
f.write(b"hello")
self.assertEqual(5, cache.uninstall(dest_dir, "1"))
self.assertEqual(
["1"], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
self.assertEqual(True, cache.cleanup())
self.assertEqual(5, cache.install(dest_dir, "1"))
self.assertEqual(5, cache.uninstall(dest_dir, "1"))
self.assertEqual(
["1"], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
self.assertEqual(
["hi"], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR, "1"))
)
def test_save_named(self):
cache = self.get_cache(_get_policies())
self.assertEqual([], sorted(fs.listdir(cache.cache_dir)))
self._add_one_item(cache, 2)
with fs.open(os.path.join(cache.cache_dir, cache.STATE_FILE)) as f:
old_content = json.load(f)
# It's immediately saved.
items = lru.LRUDict.load(os.path.join(cache.cache_dir, cache.STATE_FILE))
self.assertEqual(1, len(items))
_key, (v, _timestamp) = items.get_oldest()
# This depends on the inner format as generated by NamedCache.
entry_dir_name = v[0]
self.assertEqual(
sorted([entry_dir_name, cache.NAMED_DIR, cache.STATE_FILE]),
sorted(fs.listdir(cache.cache_dir)),
)
cache.save()
self.assertEqual(
sorted([entry_dir_name, cache.NAMED_DIR, cache.STATE_FILE]),
sorted(fs.listdir(cache.cache_dir)),
)
with fs.open(os.path.join(cache.cache_dir, cache.STATE_FILE)) as f:
new_content = json.load(f)
# That's because uninstall() called from self._add_one_item()
# causes an implicit save(). See uninstall() comments for more details.
self.assertEqual(new_content, old_content)
def test_touch(self):
cache = self.get_cache(_get_policies())
caches_to_touch = []
caches_to_touch.append(self._add_one_item(cache, 1))
caches_to_touch.append(self._add_one_item(cache, 2))
self._add_one_item(cache, 3)
cache.touch(*caches_to_touch)
cache.remove_oldest_evictable_item()
self.assertEqual(len(cache), 2)
self.assertEqual(sorted(caches_to_touch), sorted(cache))
def test_trim(self):
cache = self.get_cache(_get_policies(max_items=2))
item_count = 12
for i in range(item_count):
self._add_one_item(cache, i + 1)
self.assertEqual(len(cache), item_count)
self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], cache.trim())
self.assertEqual(len(cache), 2)
self.assertEqual(
["11", "12"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_trim_with_keep(self):
cache = self.get_named_cache(_get_policies(max_items=2), keep=["1", "2"])
item_count = 12
for i in range(item_count):
self._add_one_item(cache, i + 1)
self.assertEqual(len(cache), item_count)
self.assertEqual([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], cache.trim())
self.assertEqual(len(cache), 2)
self.assertEqual(
["1", "2"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_trim_ignores_policy_for_keep_items(self):
"""
In this case, we specify 3 items which should not be evicted but
a max_items value of 2.
Under these circumstances, trim will remove items until only the 3 items
remain.
"""
cache = self.get_named_cache(
_get_policies(max_items=2), keep=["1", "2", "3"]
)
item_count = 12
for i in range(item_count):
self._add_one_item(cache, i + 1)
self.assertEqual(len(cache), item_count)
self.assertEqual([4, 5, 6, 7, 8, 9, 10, 11, 12], cache.trim())
self.assertEqual(len(cache), 3)
self.assertEqual(
["1", "2", "3"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_trim_ignores_irrelevant_keep_items(self):
"""
Ignore dont evict if none of its items are in the cache.
"""
cache = self.get_named_cache(_get_policies(max_items=2), keep=["a", "b"])
item_count = 12
for i in range(item_count):
self._add_one_item(cache, i + 1)
self.assertEqual(len(cache), item_count)
self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], cache.trim())
self.assertEqual(len(cache), 2)
self.assertEqual(
["11", "12"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
def test_load_corrupted_state(self):
# cleanup() handles a broken state file.
fs.mkdir(self.cache_dir)
c = local_caching.NamedCache
with fs.open(os.path.join(self.cache_dir, c.STATE_FILE), "w") as f:
f.write("}}}}")
fs.makedirs(os.path.join(self.cache_dir, "1"), 0o777)
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 1)
self.assertTrue(
fs.exists(os.path.join(cache.cache_dir, cache.NAMED_DIR, "1"))
)
self.assertTrue(
fs.islink(os.path.join(cache.cache_dir, cache.NAMED_DIR, "1"))
)
self.assertEqual([], cache.trim())
self.assertTrue(
fs.exists(os.path.join(cache.cache_dir, cache.NAMED_DIR, "1"))
)
self.assertTrue(
fs.islink(os.path.join(cache.cache_dir, cache.NAMED_DIR, "1"))
)
self.assertEqual(True, cache.cleanup())
self.assertEqual(
sorted([cache.NAMED_DIR, cache.STATE_FILE, cache._lru["1"][0]]),
sorted(fs.listdir(cache.cache_dir)),
)
@unittest.skipIf(sys.platform == "win32", "crbug.com/1148174")
def test_cleanup_missing(self):
# cleanup() detects a missing item.
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 1)
file_path.rmtree(os.path.join(cache.cache_dir, cache._lru["1"][0]))
cache = self.get_cache(_get_policies())
self.assertEqual(["1"], list(cache))
self.assertEqual(True, cache.cleanup())
self.assertEqual([], list(cache))
def test_cleanup_unexpected(self):
# cleanup() delete unexpected file in the cache directory.
fs.mkdir(self.cache_dir)
with fs.open(os.path.join(self.cache_dir, "junk"), "w") as f:
f.write("random")
cache = self.get_cache(_get_policies())
self.assertEqual(["junk"], fs.listdir(cache.cache_dir))
self.assertEqual(True, cache.cleanup())
self.assertEqual([cache.STATE_FILE], fs.listdir(cache.cache_dir))
def test_cleanup_unexpected_named(self):
# cleanup() deletes unexpected symlink and directory in named/.
fs.mkdir(self.cache_dir)
c = local_caching.NamedCache
fs.mkdir(os.path.join(self.cache_dir, c.NAMED_DIR))
p = os.path.join(self.cache_dir, c.NAMED_DIR, "junk_file")
with fs.open(p, "w") as f:
f.write("random")
fs.mkdir(os.path.join(self.cache_dir, c.NAMED_DIR, "junk_dir"))
fs.symlink(
"invalid_dest", os.path.join(self.cache_dir, c.NAMED_DIR, "junk_link")
)
cache = self.get_cache(_get_policies())
self.assertEqual([cache.NAMED_DIR], fs.listdir(cache.cache_dir))
self.assertEqual(
["junk_dir", "junk_file", "junk_link"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
self.assertEqual(True, cache.cleanup())
self.assertEqual(
[cache.NAMED_DIR, cache.STATE_FILE], sorted(fs.listdir(cache.cache_dir))
)
self.assertEqual(
[], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
def test_cleanup_incorrect_link(self):
# cleanup() repairs broken symlink in named/.
cache = self.get_cache(_get_policies())
self._add_one_item(cache, 1)
self._add_one_item(cache, 2)
fs.remove(os.path.join(self.cache_dir, cache.NAMED_DIR, "1"))
fs.remove(os.path.join(self.cache_dir, cache.NAMED_DIR, "2"))
fs.symlink(
"invalid_dest", os.path.join(self.cache_dir, cache.NAMED_DIR, "1")
)
fs.mkdir(os.path.join(self.cache_dir, cache.NAMED_DIR, "2"))
cache = self.get_cache(_get_policies())
self.assertEqual(
["1", "2"],
sorted(fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))),
)
self.assertEqual(True, cache.cleanup())
self.assertEqual(
[], fs.listdir(os.path.join(cache.cache_dir, cache.NAMED_DIR))
)
def test_upgrade(self):
# Make sure upgrading works. This is temporary as eventually all bots will
# be updated.
now = time.time()
fs.mkdir(self.cache_dir)
fs.mkdir(os.path.join(self.cache_dir, "f1"))
with fs.open(os.path.join(self.cache_dir, "f1", "hello"), "wb") as f:
f.write(b"world")
# v1
old = {
"version": lru.CURRENT_VERSION,
"items": [
["cache1", [["f1", len("world")], now]],
],
}
c = local_caching.NamedCache
with fs.open(os.path.join(self.cache_dir, c.STATE_FILE), "w") as f:
json.dump(old, f)
# It automatically upgrades to v2.
cache = self.get_cache(_get_policies())
expected = {"cache1": [["f1", len("world")], now]}
self.assertEqual(expected, dict(cache._lru._items.items()))
self.assertEqual(
["f1", cache.STATE_FILE], sorted(fs.listdir(cache.cache_dir))
)
def _gen_state(items):
state = {"items": items, "version": lru.CURRENT_VERSION}
return json.dumps(state, sort_keys=True, separators=(",", ":")).encode(
"utf-8"
)
class FnTest(TestCase):
"""Test functions that leverage both DiskContentAddressedCache and
NamedCache.
"""
def setUp(self):
super(FnTest, self).setUp()
# Simulate that the memory cache used disk space.
def remove_oldest(c):
s = old_remove_oldest(c)
self._free_disk += s
return s
old_remove_oldest = self.mock(
local_caching.MemoryContentAddressedCache,
"remove_oldest_evictable_item",
remove_oldest,
)
def _prepare_cache(self, cache):
now = self._now
for i in range(1, 11):
self._add_one_item(cache, i)
self._now += 1
self._now = now
self.assertEqual([], cache.trim())
def _prepare_isolated_cache(self, cache):
self._prepare_cache(cache)
self._verify_isolated_cache(cache, range(1, 11))
def _verify_isolated_cache(self, cache, items):
# Isolated cache verification.
expected = {
self._algo(_gen_data(n)).hexdigest(): _gen_data(n) for n in items
}
expected[cache.STATE_FILE] = _gen_state(
[
[self._algo(_gen_data(n)).hexdigest(), [n, self._now + n - 1]]
for n in items
]
)
self.assertEqual(expected, read_tree(cache.cache_dir))
def _prepare_named_cache(self, cache):
self._prepare_cache(cache)
# Figure out the short names via the symlinks.
items = range(1, 11)
short_names = {
n: os.path.basename(
fs.readlink(os.path.join(cache.cache_dir, cache.NAMED_DIR, str(n)))
)
for n in items
}
self._verify_named_cache(cache, short_names, items)
return short_names
def _verify_named_cache(self, cache, short_names, items):
# Named cache verification. Ensures the cache contain the expected data.
actual = read_tree(cache.cache_dir)
# There's assumption about json encoding format but here it's good enough.
expected = {
os.path.join(short_names[n], "hello"): _gen_data(n) for n in items
}
expected[cache.STATE_FILE] = _gen_state(
[[str(n), [[short_names[n], n], self._now + n - 1]] for n in items]
)
self.assertEqual(expected, actual)
def test_clean_caches_disk(self):
# Create an isolated cache and a named cache each with 2 items. Ensure that
# one item from each is removed.
now = self._now
self._free_disk = 100000
# Setup caches.
policies = _get_policies(min_free_space=1000)
named_cache = local_caching.NamedCache(
tempfile.mkdtemp(dir=self.tempdir, prefix="nc"), policies
)
short_names = self._prepare_named_cache(named_cache)
isolated_cache = local_caching.DiskContentAddressedCache(
tempfile.mkdtemp(dir=self.tempdir, prefix="ic"), policies, trim=False
)
self._prepare_isolated_cache(isolated_cache)
self.assertEqual(now, self._now)
# Request trimming.
self._free_disk = 950
trimmed = local_caching.trim_caches(
[isolated_cache, named_cache],
self.tempdir,
min_free_space=policies.min_free_space,
max_age_secs=policies.max_age_secs,
)
# Enough to free 50 bytes. The following sums to 56.
expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7]
self.assertEqual(expected, trimmed)
# Cache verification.
self._verify_named_cache(named_cache, short_names, range(8, 11))
self._verify_isolated_cache(isolated_cache, range(8, 11))
def test_trim_named_cache_with_no_evictable_items(self):
"""Give a named cache where every entry is on the keep list. Trim must
deal with this by trimming nothing."""
policies = _get_policies(min_free_space=1)
named_cache = local_caching.NamedCache(
tempfile.mkdtemp(dir=self.tempdir, prefix="nc"),
policies,
keep=[str(x) for x in range(1, 12)],
)
self._prepare_named_cache(named_cache)
trimmed = local_caching.trim_caches(
[named_cache],
self.tempdir,
min_free_space=policies.min_free_space,
max_age_secs=policies.max_age_secs,
)
self.assertEqual([], trimmed)
def _get_5_caches(self):
# Add items from size 1 to 101 randomly into 5 caches.
caches = [
local_caching.MemoryContentAddressedCache(),
local_caching.MemoryContentAddressedCache(),
local_caching.MemoryContentAddressedCache(),
local_caching.MemoryContentAddressedCache(),
local_caching.MemoryContentAddressedCache(),
]
for i in range(100):
self._add_one_item(caches[random.randint(0, len(caches) - 1)], i + 1)
self._now += 1
return caches
def test_clean_caches_memory_size(self):
# Test that cleaning is correctly distributed independent of the cache
# location.
caches = self._get_5_caches()
# 100 bytes must be freed.
self._free_disk = 900
trimmed = local_caching.trim_caches(
caches, self.tempdir, min_free_space=1000, max_age_secs=0
)
# sum(range(1, 15)) == 105, the first value after 100.
self.assertEqual(list(range(1, 15)), trimmed)
def test_clean_caches_memory_time(self):
# Test that cleaning is correctly distributed independent of the cache
# location.
caches = self._get_5_caches()
self.mock(time, "time", lambda: self._now)
trimmed = local_caching.trim_caches(
caches, self.tempdir, min_free_space=0, max_age_secs=10
)
# Only the last 10 items are kept. The first 90 items were trimmed.
self.assertEqual(list(range(1, 91)), trimmed)
if __name__ == "__main__":
test_env.main()