summary refs log tree commit diff
path: root/tests/util/caches/test_cached_call.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/util/caches/test_cached_call.py')
-rw-r--r--tests/util/caches/test_cached_call.py161
1 files changed, 161 insertions, 0 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()