Add caching tests
This commit is contained in:
+10
-2
@@ -18,14 +18,22 @@ HTMLAttribute = tuple[str, str | int | bool]
|
||||
HTMLTag = str
|
||||
|
||||
|
||||
def _render_cached(template: str, context_json: str) -> str:
|
||||
def _render_cached_impl(template: str, context_json: str) -> str:
|
||||
context = json.loads(context_json)
|
||||
context["slot"] = mark_safe(context["slot"])
|
||||
return render_to_string(template, context)
|
||||
|
||||
|
||||
if not settings.DEBUG:
|
||||
_render_cached = lru_cache(maxsize=4096)(_render_cached)
|
||||
_render_cached = lru_cache(maxsize=4096)(_render_cached_impl)
|
||||
else:
|
||||
_render_cached = _render_cached_impl
|
||||
|
||||
|
||||
def enable_cache():
|
||||
"""Wrap _render_cached with LRU cache (for testing in DEBUG mode)."""
|
||||
global _render_cached
|
||||
_render_cached = lru_cache(maxsize=4096)(_render_cached_impl)
|
||||
|
||||
|
||||
def Component(
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import unittest
|
||||
from functools import lru_cache
|
||||
|
||||
import django
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||
django.setup()
|
||||
|
||||
from django.template import TemplateDoesNotExist
|
||||
from django.utils.safestring import SafeText
|
||||
|
||||
from common import components
|
||||
|
||||
|
||||
class RenderCachedImplTest(unittest.TestCase):
|
||||
"""Test _render_cached_impl renders templates correctly."""
|
||||
|
||||
def test_basic_render(self):
|
||||
result = components._render_cached_impl(
|
||||
"cotton/icon/play.html",
|
||||
'{"slot": "", "title": "Play"}',
|
||||
)
|
||||
self.assertIn("<svg", result)
|
||||
self.assertIn("</svg>", result)
|
||||
|
||||
def test_slot_marked_safe(self):
|
||||
result = components._render_cached_impl(
|
||||
"cotton/icon/play.html",
|
||||
'{"slot": "<b>bold</b>", "title": "Play"}',
|
||||
)
|
||||
self.assertIsInstance(result, SafeText)
|
||||
|
||||
def test_different_templates_different_output(self):
|
||||
r1 = components._render_cached_impl(
|
||||
"cotton/icon/play.html", '{"slot": "", "title": "Play"}',
|
||||
)
|
||||
r2 = components._render_cached_impl(
|
||||
"cotton/icon/delete.html", '{"slot": "", "title": "Delete"}',
|
||||
)
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test_nonexistent_template_raises(self):
|
||||
with self.assertRaises(TemplateDoesNotExist):
|
||||
components._render_cached_impl(
|
||||
"cotton/nonexistent.html", '{"slot": "", "title": "X"}',
|
||||
)
|
||||
|
||||
def test_context_keys_are_sorted(self):
|
||||
"""Verify sort_keys=True in Component produces consistent JSON."""
|
||||
from common.components import Component
|
||||
r1 = Component(
|
||||
template="cotton/icon/play.html",
|
||||
attributes=[("title", "Play"), ("b", "2")],
|
||||
)
|
||||
r2 = Component(
|
||||
template="cotton/icon/play.html",
|
||||
attributes=[("b", "2"), ("title", "Play")],
|
||||
)
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
|
||||
class RenderCachedLRUTest(unittest.TestCase):
|
||||
"""Test LRU cache behavior of _render_cached when enabled."""
|
||||
|
||||
def setUp(self):
|
||||
components.enable_cache()
|
||||
components._render_cached.cache_clear()
|
||||
|
||||
def tearDown(self):
|
||||
components._render_cached = components._render_cached_impl
|
||||
|
||||
def test_cache_hits_and_misses(self):
|
||||
# Call through _render_cached (the cached wrapper), not _render_cached_impl
|
||||
components._render_cached(
|
||||
"cotton/icon/play.html", '{"slot": "", "title": "Play"}',
|
||||
)
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertEqual(info.hits, 0)
|
||||
self.assertEqual(info.misses, 1)
|
||||
|
||||
components._render_cached(
|
||||
"cotton/icon/play.html", '{"slot": "", "title": "Play"}',
|
||||
)
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertEqual(info.hits, 1)
|
||||
self.assertEqual(info.misses, 1)
|
||||
|
||||
def test_cache_clear(self):
|
||||
components._render_cached_impl(
|
||||
"cotton/icon/play.html", '{"slot": "", "title": "Play"}',
|
||||
)
|
||||
components._render_cached.cache_clear()
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertEqual(info.currsize, 0)
|
||||
self.assertEqual(info.hits, 0)
|
||||
|
||||
def test_cache_parameters(self):
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertEqual(components._render_cached.cache_parameters()["maxsize"], 4096)
|
||||
|
||||
def test_different_contexts_different_entries(self):
|
||||
# Call through _render_cached (the cached wrapper), not _render_cached_impl
|
||||
components._render_cached(
|
||||
"cotton/button.html",
|
||||
'{"size": "base", "color": "blue", "icon": false, "class": "hover:cursor-pointer", "slot": ""}',
|
||||
)
|
||||
components._render_cached(
|
||||
"cotton/button.html",
|
||||
'{"size": "base", "color": "red", "icon": false, "class": "hover:cursor-pointer", "slot": ""}',
|
||||
)
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertEqual(info.currsize, 2)
|
||||
|
||||
def test_cache_size_limited(self):
|
||||
"""After exceeding maxsize, oldest entries are evicted."""
|
||||
for i in range(5000):
|
||||
components._render_cached_impl(
|
||||
f"cotton/icon/play.html",
|
||||
f'{{"slot": "", "title": "{i}"}}',
|
||||
)
|
||||
info = components._render_cached.cache_info()
|
||||
self.assertLessEqual(info.currsize, 4096)
|
||||
|
||||
|
||||
class ComponentIntegrationTest(unittest.TestCase):
|
||||
"""Test Component() works correctly with caching transparent."""
|
||||
|
||||
def setUp(self):
|
||||
components.enable_cache()
|
||||
components._render_cached.cache_clear()
|
||||
|
||||
def tearDown(self):
|
||||
components._render_cached = components._render_cached_impl
|
||||
|
||||
def test_template_component(self):
|
||||
result = components.Component(
|
||||
template="cotton/icon/play.html", attributes=[],
|
||||
)
|
||||
self.assertIn("<svg", result)
|
||||
self.assertIn("</svg>", result)
|
||||
|
||||
def test_tag_name_component(self):
|
||||
result = components.Component(
|
||||
tag_name="div",
|
||||
attributes=[("class", "test")],
|
||||
children="hello",
|
||||
)
|
||||
self.assertEqual(result, '<div class="test">hello</div>')
|
||||
|
||||
def test_repeated_calls_identical(self):
|
||||
r1 = components.Component(
|
||||
template="cotton/icon/play.html", attributes=[],
|
||||
)
|
||||
r2 = components.Component(
|
||||
template="cotton/icon/play.html", attributes=[],
|
||||
)
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_different_components_different(self):
|
||||
r1 = components.Component(
|
||||
template="cotton/button.html", attributes=[("hx_get", "/url1")],
|
||||
)
|
||||
r2 = components.Component(
|
||||
template="cotton/button.html", attributes=[("hx_get", "/url2")],
|
||||
)
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user