mirror of
https://github.com/django-components/django-components.git
synced 2025-07-08 01:45:00 +00:00

* feat: add benchmarking dashboard, CI hook on PR, and store lifetime results * refactor: change python env to 3.13 in benchmarks * refactor: add verbosity, use 3.11 for benchmarking * fix: OSError: [Errno 7] Argument list too long * refactor: add debug statements * refactor: remove extraneous -e * refactor: fix tests and linter errors * fix: track main package in coverage * refactor: fix test coverage testing * refactor: fix repo owner name in benchmark on pushing comment * refactor: add asv monkeypatch to docs workflow * refactor: temporarily allow building docs in forks * refactor: use py 3.13 for benchmarking * refactor: run only a single benchmark for PRs to speed them up * refactor: install asv in the docs build workflow * refactor: use hatch docs env to generate benhcmarks in docs CI * refactor: more trying * refactor: move tests * Add benchmark results for 0.137 * Trigger Build * Add benchmark results for 0.138 * refactor: set constant machine name when benchmarking * Add benchmark results for 0.139 * refactor: fix issue with paths too long * Add benchmark results for 0.140 * docs: update comment * refactor: remove test benchmarking data * refactor: fix comment * refactor: allow the benchmark workflow to write to PRs * refactor: use personal access token to set up the PR benchmark bot * refactor: split the benchmark PR flow into two to make it work with PRs from forks * refactor: update deprecated actions/upload-artifact@v3 to v4 * refactor: fix missing directory in benchmarking workflow * refactor: fix triggering of second workflow * refactor: fix workflow finally? * docs: add comments to cut-offs and direct people to benchmarks PR --------- Co-authored-by: github-actions <github-actions@github.com>
250 lines
9 KiB
Python
250 lines
9 KiB
Python
import contextlib
|
|
import functools
|
|
import sys
|
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
from unittest.mock import Mock, patch
|
|
|
|
from django.template import Context, Node
|
|
from django.template.loader import engines
|
|
from django.template.response import TemplateResponse
|
|
from django.test import SimpleTestCase, override_settings
|
|
|
|
from django_components.app_settings import ContextBehavior
|
|
from django_components.autodiscovery import autodiscover
|
|
from django_components.component_registry import registry
|
|
from django_components.middleware import ComponentDependencyMiddleware
|
|
|
|
# Create middleware instance
|
|
response_stash = None
|
|
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
|
|
|
|
|
|
class GenIdPatcher:
|
|
def __init__(self):
|
|
self._gen_id_count = 10599485
|
|
|
|
# Mock the `generate` function used inside `gen_id` so it returns deterministic IDs
|
|
def start(self):
|
|
# Random number so that the generated IDs are "hex-looking", e.g. a1bc3d
|
|
self._gen_id_count = 10599485
|
|
|
|
def mock_gen_id(*args, **kwargs):
|
|
self._gen_id_count += 1
|
|
return hex(self._gen_id_count)[2:]
|
|
|
|
self._gen_id_patch = patch("django_components.util.misc.generate", side_effect=mock_gen_id)
|
|
self._gen_id_patch.start()
|
|
|
|
def stop(self):
|
|
self._gen_id_patch.stop()
|
|
self._gen_id_count = 10599485
|
|
|
|
|
|
class CsrfTokenPatcher:
|
|
def __init__(self):
|
|
self._csrf_token = "predictabletoken"
|
|
|
|
def start(self):
|
|
self._csrf_token_patch = patch("django.middleware.csrf.get_token", return_value=self._csrf_token)
|
|
self._csrf_token_patch.start()
|
|
|
|
def stop(self):
|
|
self._csrf_token_patch.stop()
|
|
|
|
|
|
class BaseTestCase(SimpleTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.gen_id_patcher = GenIdPatcher()
|
|
self.gen_id_patcher.start()
|
|
self.csrf_token_patcher = CsrfTokenPatcher()
|
|
self.csrf_token_patcher.start()
|
|
|
|
def tearDown(self):
|
|
self.gen_id_patcher.stop()
|
|
self.csrf_token_patcher.stop()
|
|
super().tearDown()
|
|
registry.clear()
|
|
|
|
from django_components.cache import component_media_cache, template_cache
|
|
|
|
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
|
|
if template_cache:
|
|
template_cache.clear()
|
|
|
|
if component_media_cache:
|
|
component_media_cache.clear()
|
|
|
|
from django_components.component import component_node_subclasses_by_name
|
|
component_node_subclasses_by_name.clear()
|
|
|
|
|
|
request = Mock()
|
|
mock_template = Mock()
|
|
|
|
|
|
def create_and_process_template_response(template, context=None, use_middleware=True):
|
|
context = context if context is not None else Context({})
|
|
mock_template.render = lambda context, _: template.render(context)
|
|
response = TemplateResponse(request, mock_template, context)
|
|
if use_middleware:
|
|
response.render()
|
|
global response_stash
|
|
response_stash = response
|
|
response = middleware(request)
|
|
else:
|
|
response.render()
|
|
return response.content.decode("utf-8")
|
|
|
|
|
|
def print_nodes(nodes: List[Node], indent=0) -> None:
|
|
"""
|
|
Render a Nodelist, inlining child nodes with extra on separate lines and with
|
|
extra indentation.
|
|
"""
|
|
for node in nodes:
|
|
child_nodes: List[Node] = []
|
|
for attr in node.child_nodelists:
|
|
attr_child_nodes = getattr(node, attr, None) or []
|
|
if attr_child_nodes:
|
|
child_nodes.extend(attr_child_nodes)
|
|
|
|
repr = str(node)
|
|
repr = "\n".join([(" " * 4 * indent) + line for line in repr.split("\n")])
|
|
print(repr)
|
|
if child_nodes:
|
|
print_nodes(child_nodes, indent=indent + 1)
|
|
|
|
|
|
# TODO: Make sure that this is done before/after each test automatically?
|
|
@contextlib.contextmanager
|
|
def autodiscover_with_cleanup(*args, **kwargs):
|
|
"""
|
|
Use this in place of regular `autodiscover` in test files to ensure that
|
|
the autoimport does not pollute the global state.
|
|
"""
|
|
imported_modules = autodiscover(*args, **kwargs)
|
|
try:
|
|
yield imported_modules
|
|
finally:
|
|
# Teardown - delete autoimported modules, so the module is executed also the
|
|
# next time one of the tests calls `autodiscover`.
|
|
for mod in imported_modules:
|
|
del sys.modules[mod]
|
|
|
|
|
|
ContextBehStr = Union[ContextBehavior, str]
|
|
ContextBehParam = Union[ContextBehStr, Tuple[ContextBehStr, Any]]
|
|
|
|
|
|
def parametrize_context_behavior(cases: List[ContextBehParam], settings: Optional[Dict] = None):
|
|
"""
|
|
Use this decorator to run a test function with django_component's
|
|
context_behavior settings set to given values.
|
|
|
|
You can set only a single mode:
|
|
```py
|
|
@parametrize_context_behavior(["isolated"])
|
|
def test_bla_bla(self):
|
|
# do something with app_settings.CONTEXT_BEHAVIOR set
|
|
# to "isolated"
|
|
...
|
|
```
|
|
|
|
Or you can set a test to run in both modes:
|
|
```py
|
|
@parametrize_context_behavior(["django", "isolated"])
|
|
def test_bla_bla(self):
|
|
# Runs this test function twice. Once with
|
|
# app_settings.CONTEXT_BEHAVIOR set to "django",
|
|
# the other time set to "isolated"
|
|
...
|
|
```
|
|
|
|
If you need to pass parametrized data to the tests,
|
|
pass a tuple of (mode, data) instead of plain string.
|
|
To access the data as a fixture, add `context_behavior_data`
|
|
as a function argument:
|
|
```py
|
|
@parametrize_context_behavior([
|
|
("django", "result for django"),
|
|
("isolated", "result for isolated"),
|
|
])
|
|
def test_bla_bla(self, context_behavior_data):
|
|
# Runs this test function twice. Once with
|
|
# app_settings.CONTEXT_BEHAVIOR set to "django",
|
|
# the other time set to "isolated".
|
|
#
|
|
# `context_behavior_data` will first have a value
|
|
# of "result for django", then of "result for isolated"
|
|
print(context_behavior_data)
|
|
...
|
|
```
|
|
|
|
NOTE: Use only on functions and methods. This decorator was NOT tested on classes
|
|
"""
|
|
|
|
def decorator(test_func):
|
|
# NOTE: Ideally this decorator would parametrize the test function
|
|
# with `pytest.mark.parametrize`, so all test cases would be treated as separate
|
|
# tests and thus isolated. But I wasn't able to get it to work. Hence,
|
|
# as a workaround, we run multiple test cases within the same test run.
|
|
# Because of this, we need to clear the loader cache, and, on error, we need to
|
|
# propagate the info on which test case failed.
|
|
@functools.wraps(test_func)
|
|
def wrapper(self: BaseTestCase, *args, **kwargs):
|
|
for case in cases:
|
|
# Clear loader cache, see https://stackoverflow.com/a/77531127/9788634
|
|
for engine in engines.all():
|
|
engine.engine.template_loaders[0].reset()
|
|
|
|
# Reset gen_id
|
|
self.gen_id_patcher.stop()
|
|
self.gen_id_patcher.start()
|
|
|
|
# Reset template cache
|
|
from django_components.cache import component_media_cache, template_cache
|
|
|
|
if template_cache: # May be None if the cache was not initialized
|
|
template_cache.clear()
|
|
|
|
if component_media_cache:
|
|
component_media_cache.clear()
|
|
|
|
from django_components.component import component_node_subclasses_by_name
|
|
component_node_subclasses_by_name.clear()
|
|
|
|
case_has_data = not isinstance(case, str)
|
|
|
|
if isinstance(case, str):
|
|
context_beh, fixture = case, None
|
|
else:
|
|
context_beh, fixture = case
|
|
|
|
# Set `COMPONENTS={"context_behavior": context_beh}`, but do so carefully,
|
|
# so we override only that single setting, and so that we operate on copies
|
|
# to avoid spilling settings across the test cases
|
|
merged_settings = {} if not settings else settings.copy()
|
|
if "COMPONENTS" in merged_settings:
|
|
merged_settings["COMPONENTS"] = merged_settings["COMPONENTS"].copy()
|
|
else:
|
|
merged_settings["COMPONENTS"] = {}
|
|
merged_settings["COMPONENTS"]["context_behavior"] = context_beh
|
|
|
|
with override_settings(**merged_settings):
|
|
# Call the test function with the fixture as an argument
|
|
try:
|
|
if case_has_data:
|
|
test_func(self, *args, context_behavior_data=fixture, **kwargs)
|
|
else:
|
|
test_func(self, *args, **kwargs)
|
|
except Exception as err:
|
|
# Give a hint on which iteration the test failed
|
|
raise RuntimeError(
|
|
f"An error occured in test function '{test_func.__name__}' with"
|
|
f" context_behavior='{context_beh}'. See the original error above."
|
|
) from err
|
|
|
|
return wrapper
|
|
|
|
return decorator
|