diff --git a/tests/util/caches/test_cached_call.py b/tests/util/caches/test_cached_call.py
new file mode 100644
index 0000000000..f349b5ced0
--- /dev/null
+++ b/tests/util/caches/test_cached_call.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest.mock import Mock
+
+from twisted.internet import defer
+from twisted.internet.defer import Deferred
+
+from synapse.util.caches.cached_call import CachedCall, RetryOnExceptionCachedCall
+
+from tests.test_utils import get_awaitable_result
+from tests.unittest import TestCase
+
+
+class CachedCallTestCase(TestCase):
+ def test_get(self):
+ """
+ Happy-path test case: makes a couple of calls and makes sure they behave
+ correctly
+ """
+ d = Deferred()
+
+ async def f():
+ return await d
+
+ slow_call = Mock(side_effect=f)
+
+ cached_call = CachedCall(slow_call)
+
+ # the mock should not yet have been called
+ slow_call.assert_not_called()
+
+ # now fire off a couple of calls
+ completed_results = []
+
+ async def r():
+ res = await cached_call.get()
+ completed_results.append(res)
+
+ r1 = defer.ensureDeferred(r())
+ r2 = defer.ensureDeferred(r())
+
+ # neither result should be complete yet
+ self.assertNoResult(r1)
+ self.assertNoResult(r2)
+
+ # and the mock should have been called *once*, with no params
+ slow_call.assert_called_once_with()
+
+ # allow the deferred to complete, which should complete both the pending results
+ d.callback(123)
+ self.assertEqual(completed_results, [123, 123])
+ self.successResultOf(r1)
+ self.successResultOf(r2)
+
+ # another call to the getter should complete immediately
+ slow_call.reset_mock()
+ r3 = get_awaitable_result(cached_call.get())
+ self.assertEqual(r3, 123)
+ slow_call.assert_not_called()
+
+ def test_fast_call(self):
+ """
+ Test the behaviour when the underlying function completes immediately
+ """
+
+ async def f():
+ return 12
+
+ fast_call = Mock(side_effect=f)
+ cached_call = CachedCall(fast_call)
+
+ # the mock should not yet have been called
+ fast_call.assert_not_called()
+
+ # run the call a couple of times, which should complete immediately
+ self.assertEqual(get_awaitable_result(cached_call.get()), 12)
+ self.assertEqual(get_awaitable_result(cached_call.get()), 12)
+
+ # the mock should have been called once
+ fast_call.assert_called_once_with()
+
+
+class RetryOnExceptionCachedCallTestCase(TestCase):
+ def test_get(self):
+ # set up the RetryOnExceptionCachedCall around a function which will fail
+ # (after a while)
+ d = Deferred()
+
+ async def f1():
+ await d
+ raise ValueError("moo")
+
+ slow_call = Mock(side_effect=f1)
+ cached_call = RetryOnExceptionCachedCall(slow_call)
+
+ # the mock should not yet have been called
+ slow_call.assert_not_called()
+
+ # now fire off a couple of calls
+ completed_results = []
+
+ async def r():
+ try:
+ await cached_call.get()
+ except Exception as e1:
+ completed_results.append(e1)
+
+ r1 = defer.ensureDeferred(r())
+ r2 = defer.ensureDeferred(r())
+
+ # neither result should be complete yet
+ self.assertNoResult(r1)
+ self.assertNoResult(r2)
+
+ # and the mock should have been called *once*, with no params
+ slow_call.assert_called_once_with()
+
+ # complete the deferred, which should make the pending calls fail
+ d.callback(0)
+ self.assertEqual(len(completed_results), 2)
+ for e in completed_results:
+ self.assertIsInstance(e, ValueError)
+ self.assertEqual(e.args, ("moo",))
+
+ # reset the mock to return a successful result, and make another pair of calls
+ # to the getter
+ d = Deferred()
+
+ async def f2():
+ return await d
+
+ slow_call.reset_mock()
+ slow_call.side_effect = f2
+ r3 = defer.ensureDeferred(cached_call.get())
+ r4 = defer.ensureDeferred(cached_call.get())
+
+ self.assertNoResult(r3)
+ self.assertNoResult(r4)
+ slow_call.assert_called_once_with()
+
+ # let that call complete, and check the results
+ d.callback(123)
+ self.assertEqual(self.successResultOf(r3), 123)
+ self.assertEqual(self.successResultOf(r4), 123)
+
+ # and now more calls to the getter should complete immediately
+ slow_call.reset_mock()
+ self.assertEqual(get_awaitable_result(cached_call.get()), 123)
+ slow_call.assert_not_called()
diff --git a/tests/util/caches/test_deferred_cache.py b/tests/util/caches/test_deferred_cache.py
index ecd9efc4df..c24c33ee91 100644
--- a/tests/util/caches/test_deferred_cache.py
+++ b/tests/util/caches/test_deferred_cache.py
@@ -232,7 +232,10 @@ class DeferredCacheTestCase(TestCase):
def test_eviction_iterable(self):
cache = DeferredCache(
- "test", max_entries=3, apply_cache_factor_from_config=False, iterable=True,
+ "test",
+ max_entries=3,
+ apply_cache_factor_from_config=False,
+ iterable=True,
)
cache.prefill(1, ["one", "two"])
diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py
index cf1e3203a4..afb11b9caf 100644
--- a/tests/util/caches/test_descriptors.py
+++ b/tests/util/caches/test_descriptors.py
@@ -143,8 +143,7 @@ class DescriptorTestCase(unittest.TestCase):
obj.mock.assert_not_called()
def test_cache_with_sync_exception(self):
- """If the wrapped function throws synchronously, things should continue to work
- """
+ """If the wrapped function throws synchronously, things should continue to work"""
class Cls:
@cached()
@@ -165,8 +164,7 @@ class DescriptorTestCase(unittest.TestCase):
self.failureResultOf(d, SynapseError)
def test_cache_with_async_exception(self):
- """The wrapped function returns a failure
- """
+ """The wrapped function returns a failure"""
class Cls:
result = None
@@ -282,7 +280,8 @@ class DescriptorTestCase(unittest.TestCase):
try:
d = obj.fn(1)
self.assertEqual(
- current_context(), SENTINEL_CONTEXT,
+ current_context(),
+ SENTINEL_CONTEXT,
)
yield d
self.fail("No exception thrown")
@@ -374,8 +373,7 @@ class DescriptorTestCase(unittest.TestCase):
obj.mock.assert_not_called()
def test_cache_iterable_with_sync_exception(self):
- """If the wrapped function throws synchronously, things should continue to work
- """
+ """If the wrapped function throws synchronously, things should continue to work"""
class Cls:
@descriptors.cached(iterable=True)
diff --git a/tests/util/test_itertools.py b/tests/util/test_itertools.py
index 1ef0af8e8f..e931a7ec18 100644
--- a/tests/util/test_itertools.py
+++ b/tests/util/test_itertools.py
@@ -24,28 +24,32 @@ class ChunkSeqTests(TestCase):
parts = chunk_seq("123", 8)
self.assertEqual(
- list(parts), ["123"],
+ list(parts),
+ ["123"],
)
def test_long_seq(self):
parts = chunk_seq("abcdefghijklmnop", 8)
self.assertEqual(
- list(parts), ["abcdefgh", "ijklmnop"],
+ list(parts),
+ ["abcdefgh", "ijklmnop"],
)
def test_uneven_parts(self):
parts = chunk_seq("abcdefghijklmnop", 5)
self.assertEqual(
- list(parts), ["abcde", "fghij", "klmno", "p"],
+ list(parts),
+ ["abcde", "fghij", "klmno", "p"],
)
def test_empty_input(self):
parts = chunk_seq([], 5)
self.assertEqual(
- list(parts), [],
+ list(parts),
+ [],
)
diff --git a/tests/util/test_stream_change_cache.py b/tests/util/test_stream_change_cache.py
index 13b753e367..9ed01f7e0c 100644
--- a/tests/util/test_stream_change_cache.py
+++ b/tests/util/test_stream_change_cache.py
@@ -70,7 +70,8 @@ class StreamChangeCacheTests(unittest.HomeserverTestCase):
self.assertTrue("user@foo.com" not in cache._entity_to_key)
self.assertEqual(
- cache.get_all_entities_changed(2), ["bar@baz.net", "user@elsewhere.org"],
+ cache.get_all_entities_changed(2),
+ ["bar@baz.net", "user@elsewhere.org"],
)
self.assertIsNone(cache.get_all_entities_changed(1))
@@ -80,7 +81,8 @@ class StreamChangeCacheTests(unittest.HomeserverTestCase):
{"bar@baz.net", "user@elsewhere.org"}, set(cache._entity_to_key)
)
self.assertEqual(
- cache.get_all_entities_changed(2), ["user@elsewhere.org", "bar@baz.net"],
+ cache.get_all_entities_changed(2),
+ ["user@elsewhere.org", "bar@baz.net"],
)
self.assertIsNone(cache.get_all_entities_changed(1))
@@ -222,7 +224,8 @@ class StreamChangeCacheTests(unittest.HomeserverTestCase):
# Query a subset of the entries mid-way through the stream. We should
# only get back the subset.
self.assertEqual(
- cache.get_entities_changed(["bar@baz.net"], stream_pos=2), {"bar@baz.net"},
+ cache.get_entities_changed(["bar@baz.net"], stream_pos=2),
+ {"bar@baz.net"},
)
def test_max_pos(self):
|