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