diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py index b9eddabd6..419463bea 100644 --- a/src/borg/testsuite/remote.py +++ b/src/borg/testsuite/remote.py @@ -1,9 +1,13 @@ +import errno import os import time +from unittest.mock import patch import pytest -from ..remote import SleepingBandwidthLimiter +from ..remote import SleepingBandwidthLimiter, RepositoryCache +from ..repository import Repository +from .hashindex import H class TestSleepingBandwidthLimiter: @@ -58,3 +62,88 @@ def test_write(self, monkeypatch): now += 10 self.expect_write(5, b"1") it.write(5, b"1") + + +class TestRepositoryCache: + @pytest.yield_fixture + def repository(self, tmpdir): + self.repository_location = os.path.join(str(tmpdir), 'repository') + with Repository(self.repository_location, exclusive=True, create=True) as repository: + repository.put(H(1), b'1234') + repository.put(H(2), b'5678') + repository.put(H(3), bytes(100)) + yield repository + + @pytest.fixture + def cache(self, repository): + return RepositoryCache(repository) + + def test_simple(self, cache: RepositoryCache): + # Single get()s are not cached, since they are used for unique objects like archives. + assert cache.get(H(1)) == b'1234' + assert cache.misses == 1 + assert cache.hits == 0 + + assert list(cache.get_many([H(1)])) == [b'1234'] + assert cache.misses == 2 + assert cache.hits == 0 + + assert list(cache.get_many([H(1)])) == [b'1234'] + assert cache.misses == 2 + assert cache.hits == 1 + + assert cache.get(H(1)) == b'1234' + assert cache.misses == 2 + assert cache.hits == 2 + + def test_backoff(self, cache: RepositoryCache): + def query_size_limit(): + cache.size_limit = 0 + + assert list(cache.get_many([H(1), H(2)])) == [b'1234', b'5678'] + assert cache.misses == 2 + assert cache.evictions == 0 + iterator = cache.get_many([H(1), H(3), H(2)]) + assert next(iterator) == b'1234' + + # Force cache to back off + qsl = cache.query_size_limit + cache.query_size_limit = query_size_limit + cache.backoff() + cache.query_size_limit = qsl + # Evicted H(1) and H(2) + assert cache.evictions == 2 + assert H(1) not in cache.cache + assert H(2) not in cache.cache + assert next(iterator) == bytes(100) + assert cache.slow_misses == 0 + # Since H(2) was in the cache when we called get_many(), but has + # been evicted during iterating the generator, it will be a slow miss. + assert next(iterator) == b'5678' + assert cache.slow_misses == 1 + + def test_enospc(self, cache: RepositoryCache): + class enospc_open: + def __init__(self, *args): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def write(self, data): + raise OSError(errno.ENOSPC, 'foo') + + iterator = cache.get_many([H(1), H(2), H(3)]) + assert next(iterator) == b'1234' + + with patch('builtins.open', enospc_open): + assert next(iterator) == b'5678' + assert cache.enospc == 1 + # We didn't patch query_size_limit which would set size_limit to some low + # value, so nothing was actually evicted. + assert cache.evictions == 0 + + assert next(iterator) == bytes(100)