summary refs log tree commit diff
path: root/tests/util
diff options
context:
space:
mode:
Diffstat (limited to 'tests/util')
-rw-r--r--tests/util/caches/test_cached_call.py161
-rw-r--r--tests/util/caches/test_deferred_cache.py5
-rw-r--r--tests/util/caches/test_descriptors.py12
-rw-r--r--tests/util/test_itertools.py12
-rw-r--r--tests/util/test_stream_change_cache.py9
5 files changed, 184 insertions, 15 deletions
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):