Cleanup and remove deprecated RequestCache Django app
ARCH-223
This commit is contained in:
committed by
Robert Raposa
parent
53d8a04b88
commit
700a902b68
@@ -1,65 +1,297 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for cache_utils.py
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
import ddt
|
||||
from mock import MagicMock
|
||||
from mock import Mock
|
||||
|
||||
from openedx.core.lib.cache_utils import memoize_in_request_cache
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMemoizeInRequestCache(TestCase):
|
||||
class TestRequestCachedDecorator(TestCase):
|
||||
"""
|
||||
Test the memoize_in_request_cache helper function.
|
||||
Test the request_cached decorator.
|
||||
"""
|
||||
class TestCache(object):
|
||||
"""
|
||||
A test cache that provides a data dict for caching values, analogous to the request_cache.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
|
||||
def setUp(self):
|
||||
super(TestMemoizeInRequestCache, self).setUp()
|
||||
self.request_cache = self.TestCache()
|
||||
RequestCache.clear_all_namespaces()
|
||||
|
||||
@memoize_in_request_cache('request_cache')
|
||||
def func_to_memoize(self, param):
|
||||
def test_request_cached_miss_and_then_hit(self):
|
||||
"""
|
||||
A test function whose results are to be memoized in the request_cache.
|
||||
Ensure that after a cache miss, we fill the cache and can hit it.
|
||||
"""
|
||||
return self.func_to_count(param)
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.return_value = 42
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
@memoize_in_request_cache('request_cache')
|
||||
def multi_param_func_to_memoize(self, param1, param2):
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
wrapped = request_cached()(mock_wrapper)
|
||||
result = wrapped()
|
||||
self.assertEqual(result, 42)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
result = wrapped()
|
||||
self.assertEqual(result, 42)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
def test_request_cached_with_caches_despite_changing_wrapped_result(self):
|
||||
"""
|
||||
A test function with multiple parameters whose results are to be memoized in the request_cache.
|
||||
Ensure that after caching a result, we always send it back, even if the underlying result changes.
|
||||
"""
|
||||
return self.func_to_count(param1, param2)
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [1, 2, 3]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
def test_memoize_in_request_cache(self):
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
wrapped = request_cached()(mock_wrapper)
|
||||
result = wrapped()
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
result = wrapped()
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
direct_result = mock_wrapper()
|
||||
self.assertEqual(direct_result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
result = wrapped()
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
direct_result = mock_wrapper()
|
||||
self.assertEqual(direct_result, 3)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
def test_request_cached_with_changing_args(self):
|
||||
"""
|
||||
Tests the memoize_in_request_cache decorator for both single-param and multiple-param functions.
|
||||
Ensure that calling a decorated function with different positional arguments
|
||||
will not use a cached value invoked by a previous call with different arguments.
|
||||
"""
|
||||
funcs_to_test = (
|
||||
(self.func_to_memoize, ['foo'], ['bar']),
|
||||
(self.multi_param_func_to_memoize, ['foo', 'foo2'], ['foo', 'foo3']),
|
||||
)
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [1, 2, 3, 4, 5, 6]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
for func_to_memoize, arg_list1, arg_list2 in funcs_to_test:
|
||||
self.func_to_count = MagicMock() # pylint: disable=attribute-defined-outside-init
|
||||
self.assertFalse(self.func_to_count.called)
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
func_to_memoize(*arg_list1)
|
||||
self.func_to_count.assert_called_once_with(*arg_list1)
|
||||
wrapped = request_cached()(mock_wrapper)
|
||||
|
||||
func_to_memoize(*arg_list1)
|
||||
self.func_to_count.assert_called_once_with(*arg_list1)
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
for _ in range(10):
|
||||
func_to_memoize(*arg_list1)
|
||||
func_to_memoize(*arg_list2)
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
self.assertEquals(self.func_to_count.call_count, 2)
|
||||
# This is bypass of the decorator.
|
||||
direct_result = mock_wrapper(3)
|
||||
self.assertEqual(direct_result, 3)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
# These will be hits, and not make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
def test_request_cached_with_changing_kwargs(self):
|
||||
"""
|
||||
Ensure that calling a decorated function with different keyword arguments
|
||||
will not use a cached value invoked by a previous call with different arguments.
|
||||
"""
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [1, 2, 3, 4, 5, 6]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
wrapped = request_cached()(mock_wrapper)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(1, foo=1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(2, foo=2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
# This is bypass of the decorator.
|
||||
direct_result = mock_wrapper(3, foo=3)
|
||||
self.assertEqual(direct_result, 3)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
# These will be hits, and not make an underlying call.
|
||||
result = wrapped(1, foo=1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
result = wrapped(2, foo=2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
# Since we're changing foo, this will be a miss.
|
||||
result = wrapped(2, foo=5)
|
||||
self.assertEqual(result, 4)
|
||||
self.assertEqual(to_be_wrapped.call_count, 4)
|
||||
|
||||
# Since we're adding bar, this will be a miss.
|
||||
result = wrapped(2, foo=1, bar=2)
|
||||
self.assertEqual(result, 5)
|
||||
self.assertEqual(to_be_wrapped.call_count, 5)
|
||||
|
||||
# Should be a hit, even when kwargs are in a different order
|
||||
result = wrapped(2, bar=2, foo=1)
|
||||
self.assertEqual(result, 5)
|
||||
self.assertEqual(to_be_wrapped.call_count, 5)
|
||||
|
||||
def test_request_cached_mixed_unicode_str_args(self):
|
||||
"""
|
||||
Ensure that request_cached can work with mixed str and Unicode parameters.
|
||||
"""
|
||||
def dummy_function(arg1, arg2):
|
||||
"""
|
||||
A dummy function that expects an str and unicode arguments.
|
||||
"""
|
||||
assert isinstance(arg1, str), 'First parameter has to be of type `str`'
|
||||
assert isinstance(arg2, unicode), 'Second parameter has to be of type `unicode`'
|
||||
return True
|
||||
|
||||
self.assertTrue(dummy_function('Hello', u'World'), 'Should be callable with ASCII chars')
|
||||
self.assertTrue(dummy_function('H∂llå', u'Wørld'), 'Should be callable with non-ASCII chars')
|
||||
|
||||
wrapped = request_cached()(dummy_function)
|
||||
|
||||
self.assertTrue(wrapped('Hello', u'World'), 'Wrapper should handle ASCII only chars')
|
||||
self.assertTrue(wrapped('H∂llå', u'Wørld'), 'Wrapper should handle non-ASCII chars')
|
||||
|
||||
def test_request_cached_with_none_result(self):
|
||||
"""
|
||||
Ensure that calling a decorated function that returns None
|
||||
properly caches the result and doesn't recall the underlying
|
||||
function.
|
||||
"""
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [None, None, None, 1, 1]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
wrapped = request_cached()(mock_wrapper)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, None)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, None)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
# This is bypass of the decorator.
|
||||
direct_result = mock_wrapper(3)
|
||||
self.assertEqual(direct_result, None)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
# These will be hits, and not make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, None)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, None)
|
||||
self.assertEqual(to_be_wrapped.call_count, 3)
|
||||
|
||||
def test_request_cached_with_request_cache_getter(self):
|
||||
"""
|
||||
Ensure that calling a decorated function uses
|
||||
request_cache_getter if supplied.
|
||||
"""
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [1, 2, 3]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
request_cache_getter = lambda args, kwargs: RequestCache('test')
|
||||
wrapped = request_cached(request_cache_getter=request_cache_getter)(mock_wrapper)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
# These will be a hits, and not make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
# Ensure the appropriate request cache was used
|
||||
self.assertFalse(RequestCache().data)
|
||||
self.assertTrue(RequestCache('test').data)
|
||||
|
||||
def test_request_cached_with_arg_map_function(self):
|
||||
"""
|
||||
Ensure that calling a decorated function uses
|
||||
arg_map_function to determined the cache key.
|
||||
"""
|
||||
to_be_wrapped = Mock()
|
||||
to_be_wrapped.side_effect = [1, 2, 3]
|
||||
self.assertEqual(to_be_wrapped.call_count, 0)
|
||||
|
||||
def mock_wrapper(*args, **kwargs):
|
||||
"""Simple wrapper to let us decorate our mock."""
|
||||
return to_be_wrapped(*args, **kwargs)
|
||||
|
||||
arg_map_function = lambda arg: unicode(arg == 1)
|
||||
wrapped = request_cached(arg_map_function=arg_map_function)(mock_wrapper)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 1)
|
||||
|
||||
# This will be a miss, and make an underlying call.
|
||||
result = wrapped(2)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
# These will be a hits, and not make an underlying call.
|
||||
result = wrapped(1)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
result = wrapped(3)
|
||||
self.assertEqual(result, 2)
|
||||
self.assertEqual(to_be_wrapped.call_count, 2)
|
||||
|
||||
84
openedx/core/lib/tests/test_request_utils.py
Normal file
84
openedx/core/lib/tests/test_request_utils.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Tests for request_utils module."""
|
||||
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from openedx.core.lib.request_utils import get_request_or_stub, course_id_from_url, safe_get_host
|
||||
|
||||
|
||||
class RequestUtilTestCase(unittest.TestCase):
|
||||
"""
|
||||
Tests for request_utils module.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(RequestUtilTestCase, self).setUp()
|
||||
self.old_site_name = settings.SITE_NAME
|
||||
self.old_allowed_hosts = settings.ALLOWED_HOSTS
|
||||
|
||||
def tearDown(self):
|
||||
super(RequestUtilTestCase, self).tearDown()
|
||||
settings.SITE_NAME = self.old_site_name
|
||||
settings.ALLOWED_HOSTS = self.old_allowed_hosts
|
||||
|
||||
def test_get_request_or_stub(self):
|
||||
"""
|
||||
Outside the context of the request, we should still get a request
|
||||
that allows us to build an absolute URI.
|
||||
"""
|
||||
stub = get_request_or_stub()
|
||||
expected_url = "http://{site_name}/foobar".format(site_name=settings.SITE_NAME)
|
||||
self.assertEqual(stub.build_absolute_uri("foobar"), expected_url)
|
||||
|
||||
def test_safe_get_host(self):
|
||||
""" Tests that the safe_get_host function returns the desired host """
|
||||
settings.SITE_NAME = 'siteName.com'
|
||||
factory = RequestFactory()
|
||||
request = factory.request()
|
||||
request.META['HTTP_HOST'] = 'www.userProvidedHost.com'
|
||||
# If ALLOWED_HOSTS is not set properly, safe_get_host should return SITE_NAME
|
||||
settings.ALLOWED_HOSTS = None
|
||||
self.assertEqual(safe_get_host(request), "siteName.com")
|
||||
settings.ALLOWED_HOSTS = ["*"]
|
||||
self.assertEqual(safe_get_host(request), "siteName.com")
|
||||
settings.ALLOWED_HOSTS = ["foo.com", "*"]
|
||||
self.assertEqual(safe_get_host(request), "siteName.com")
|
||||
|
||||
# If ALLOWED_HOSTS is set properly, and the host is valid, we just return the user-provided host
|
||||
settings.ALLOWED_HOSTS = [request.META['HTTP_HOST']]
|
||||
self.assertEqual(safe_get_host(request), request.META['HTTP_HOST'])
|
||||
|
||||
# If ALLOWED_HOSTS is set properly but the host is invalid, we should get a SuspiciousOperation
|
||||
settings.ALLOWED_HOSTS = ["the_valid_website.com"]
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
safe_get_host(request)
|
||||
|
||||
def test_course_id_from_url(self):
|
||||
""" Test course_id_from_url(). """
|
||||
|
||||
self.assertIsNone(course_id_from_url('/login'))
|
||||
self.assertIsNone(course_id_from_url('/course/edX/maths/2020'))
|
||||
self.assertIsNone(course_id_from_url('/courses/edX/maths/'))
|
||||
self.assertIsNone(course_id_from_url('/api/courses/v1/blocks/edX/maths/2020'))
|
||||
self.assertIsNone(course_id_from_url('/api/courses/v1/blocks/course-v1:incidental+courseid+formatting'))
|
||||
self.assertIsNone(course_id_from_url('/api/courses/v41/notcourses/course-v1:incidental+courseid+formatting'))
|
||||
|
||||
course_id = course_id_from_url('/courses/course-v1:edX+maths+2020')
|
||||
self.assertCourseIdFieldsMatch(course_id=course_id, org="edX", course='maths', run='2020')
|
||||
|
||||
course_id = course_id_from_url('/courses/edX/maths/2020')
|
||||
self.assertCourseIdFieldsMatch(course_id=course_id, org='edX', course='maths', run='2020')
|
||||
|
||||
course_id = course_id_from_url('/api/courses/v1/courses/course-v1:edX+maths+2020')
|
||||
self.assertCourseIdFieldsMatch(course_id=course_id, org='edX', course='maths', run='2020')
|
||||
|
||||
course_id = course_id_from_url('/api/courses/v1/courses/edX/maths/2020')
|
||||
self.assertCourseIdFieldsMatch(course_id=course_id, org='edX', course='maths', run='2020')
|
||||
|
||||
def assertCourseIdFieldsMatch(self, course_id, org, course, run):
|
||||
""" Asserts that the passed-in course id matches the specified fields"""
|
||||
self.assertEqual(course_id.org, org)
|
||||
self.assertEqual(course_id.course, course)
|
||||
self.assertEqual(course_id.run, run)
|
||||
Reference in New Issue
Block a user