diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 31a967aac..5caef3ad8 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -327,6 +327,75 @@ def restore(): request.addfinalizer(restore) +@pytest.fixture(autouse=True, scope='session') +def _fail_for_invalid_template_variable(request): + """Fixture that fails for invalid variables in templates. + + This fixture will fail each test that uses django template rendering + should at template contain an invalid template variable. + + It does not raise an exception, but fails, as the stack trace doesn't + offer any helpful information to debug. + This behavior can be switched of using the marker: + ``ignore_template_errors`` + """ + class InvalidVarException(object): + + """Custom handler for invalid strings in templates.""" + + def __contains__(self, key): + """There is a test for '%s' in TEMPLATE_STRING_IF_INVALID.""" + return key == '%s' + + def _get_template(self): + import inspect + from functools import reduce + + from django.template import Template + + stack = inspect.stack() + # finding the ``render`` needle in the stack + frame = reduce( + lambda x, y: y[3] == 'render' and 'base.py' in y[1] and y or x, + stack + ) + # assert 0, stack + frame = frame[0] + # finding only the frame locals in all frame members + f_locals = reduce( + lambda x, y: y[0] == 'f_locals' and y or x, + inspect.getmembers(frame) + )[1] + # ``django.template.base.Template`` + template = f_locals['self'] + if isinstance(template, Template): + return template + + def __mod__(self, var): + """Handle TEMPLATE_STRING_IF_INVALID % var.""" + template = self._get_template() + if template: + msg = "Undefined template variable '%s' in '%s'" % (var, template.name) + else: + msg = "Undefined template variable '%s'" % var + pytest.fail(msg, pytrace=False) + + if django_settings_is_configured(): + from django.conf import settings + + settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException() + + +@pytest.fixture(autouse=True) +def _template_string_if_invalid_marker(request): + """Apply the @pytest.mark.ignore_template_errors marker, + internal to pytest-django.""" + marker = request.keywords.get('ignore_template_errors', None) + if marker and django_settings_is_configured(): + from django.conf import settings + + settings.TEMPLATE_STRING_IF_INVALID = None + # ############### Helper Functions ################ diff --git a/tests/test_environment.py b/tests/test_environment.py index b7c40ce7b..c784f6c02 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -28,6 +28,59 @@ def test_mail_again(): test_mail() +@pytest.mark.django_project(extra_settings=""" + INSTALLED_APPS = [ + 'tpkg.app', + ] + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ) + TEMPLATE_DIRS = ( + ) + ROOT_URLCONF = 'tpkg.app.urls' + """) +def test_invalid_template_variable(django_testdir): + django_testdir.create_app_file(""" + try: + from django.conf.urls import patterns # Django >1.4 + except ImportError: + from django.conf.urls.defaults import patterns # Django 1.3 + + urlpatterns = patterns( + '', + (r'invalid_template/', 'tpkg.app.views.invalid_template'), + ) + """, 'urls.py') + django_testdir.create_app_file(""" + from django.shortcuts import render + + + def invalid_template(request): + return render(request, 'invalid_template.html', {}) + """, 'views.py') + django_testdir.create_app_file( + "
{{ invalid_var }}
", + 'templates/invalid_template.html' + ) + django_testdir.create_test_module(''' + import pytest + + def test_for_invalid_template(client): + client.get('/invalid_template/') + + @pytest.mark.ignore_template_errors + def test_ignore(client): + client.get('/invalid_template/') + ''') + result = django_testdir.runpytest('-s') + result.stdout.fnmatch_lines_random([ + "tpkg/test_the_test.py F.", + "Undefined template variable 'invalid_var' in 'invalid_template.html'", + ]) + + + @pytest.mark.django_db def test_database_rollback(): assert Item.objects.count() == 0