Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds tests for invalid template variables #222

Merged
merged 1 commit into from
Jul 25, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ on what marks are and for notes on using_ them.
assert 'Success!' in client.get('/some_url_defined_in_test_urls/')


``pytest.mark.ignore_template_errors`` - ignore invalid template variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

..py:function:: pytest.mark.ignore_template_errors

If you run py.test using the ``--fail-on-template-vars`` option,
tests will fail should your templates contain any invalid variables.
This marker will disable this feature by setting ``settings.TEMPLATE_STRING_IF_INVALID=None``
or the ``string_if_invalid`` template option in Django>=1.7

Example usage::

@pytest.mark.ignore_template_errors
def test_something(client):
client('some-url-with-invalid-template-vars')


Fixtures
--------

Expand All @@ -86,7 +103,7 @@ More information on fixtures is available in the `py.test documentation


``rf`` - ``RequestFactory``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~

An instance of a `django.test.RequestFactory`_

Expand Down
7 changes: 7 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ the command line::
See the `py.test documentation on Usage and invocations
<http://pytest.org/latest/usage.html>`_ for more help on available parameters.

Additional command line options
-------------------------------

``--fail-on-template-vars`` - fail for invalid variables in templates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Fail tests that render templates which make use of invalid template variables.

Running tests in parallel with pytest-xdist
-------------------------------------------
pytest-django supports running tests on multiple processes to speed up test
Expand Down
99 changes: 99 additions & 0 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""

import contextlib
import inspect
from functools import reduce
import os
import sys
import types
Expand All @@ -27,6 +29,7 @@

SETTINGS_MODULE_ENV = 'DJANGO_SETTINGS_MODULE'
CONFIGURATION_ENV = 'DJANGO_CONFIGURATION'
INVALID_TEMPLATE_VARS_ENV = 'FAIL_INVALID_TEMPLATE_VARS'


# ############### pytest hooks ################
Expand Down Expand Up @@ -62,6 +65,12 @@ def pytest_addoption(parser):
'Automatically find and add a Django project to the '
'Python path.',
default=True)
group._addoption('--fail-on-template-vars',
action='store_true', dest='itv', default=False,
help='Fail for invalid variables in templates.')
parser.addini(INVALID_TEMPLATE_VARS_ENV,
'Fail for invalid variables in templates.',
default=False)


def _exists(path, ignore=EnvironmentError):
Expand Down Expand Up @@ -170,6 +179,14 @@ def pytest_load_initial_conftests(early_config, parser, args):
else:
_django_project_scan_outcome = PROJECT_SCAN_DISABLED

# Configure FAIL_INVALID_TEMPLATE_VARS
itv = (options.itv or
os.environ.get(INVALID_TEMPLATE_VARS_ENV) in ['true', 'True', '1'] or
early_config.getini(INVALID_TEMPLATE_VARS_ENV))

if itv:
os.environ[INVALID_TEMPLATE_VARS_ENV] = 'true'

# Configure DJANGO_SETTINGS_MODULE
ds = (options.ds or
os.environ.get(SETTINGS_MODULE_ENV) or
Expand Down Expand Up @@ -327,6 +344,88 @@ 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 a template contain an invalid template variable.
The fail message will include the name of the invalid variable and
in most cases the template name.

It does not raise an exception, but fails, as the stack trace doesn't
offer any helpful information to debug.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mention that it fails with a message, possibly mentioning the template, but at least the variable.
My first impression from reading was that it would "just fail".

This behavior can be switched off using the marker:
``ignore_template_errors``
"""
class InvalidVarException(object):
"""Custom handler for invalid strings in templates."""

def __init__(self):
self.fail = True

def __contains__(self, key):
"""There is a test for '%s' in TEMPLATE_STRING_IF_INVALID."""
return key == '%s'

def _get_template(self):
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
if self.fail:
pytest.fail(msg, pytrace=False)
Copy link
Contributor

@blueyed blueyed Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codingjoe
Do you remember why you used pytrace=False here?
I think it is very valuable to have a traceback in case of errors, isn't it?
(it might need to be massaged a bit to make it shorter though).
(via #470)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I don't. I think back then there trace didn't give you much information, because the template engine worked differently. That was before you could switch engines, if I remember correctly.

Copy link
Contributor

@blueyed blueyed Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codingjoe
Thanks for your feedback, enabled it in 760fb18 (#470).

else:
return msg
if os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true':
if django_settings_is_configured():
import django
from django.conf import settings

if django.VERSION >= (1, 8) and settings.TEMPLATES:
settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidVarException()
else:
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 os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true':
if marker and django_settings_is_configured():
import django
from django.conf import settings

if django.VERSION >= (1, 8) and settings.TEMPLATES:
settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'].fail = False
else:
settings.TEMPLATE_STRING_IF_INVALID.fail = False

# ############### Helper Functions ################


Expand Down
99 changes: 99 additions & 0 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,105 @@ 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',
)
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(
"<div>{{ invalid_var }}</div>",
'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', '--fail-on-template-vars')
result.stdout.fnmatch_lines_random([
"tpkg/test_the_test.py F.",
"Undefined template variable 'invalid_var' in 'invalid_template.html'",
])


@pytest.mark.django_project(extra_settings="""
INSTALLED_APPS = [
'tpkg.app',
]
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
ROOT_URLCONF = 'tpkg.app.urls'
""")
def test_invalid_template_variable_opt_in(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(
"<div>{{ invalid_var }}</div>",
'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 ..",
])


@pytest.mark.django_db
def test_database_rollback():
assert Item.objects.count() == 0
Expand Down