from __future__ import with_statement, unicode_literals
import io
import os
import sys
from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
from django.utils import six, unittest
from compressor.cache import flush_offline_manifest, get_offline_manifest
from compressor.conf import settings
from compressor.exceptions import OfflineGenerationError
from compressor.management.commands.compress import Command as CompressCommand
from compressor.storage import default_storage
if six.PY3:
# there is an 'io' module in python 2.6+, but io.StringIO does not
# accept regular strings, just unicode objects
from io import StringIO
else:
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
# The Jinja2 tests fail on Python 3.2 due to the following:
# The line in compressor/management/commands/compress.py:
# compressor_nodes.setdefault(template, []).extend(nodes)
# causes the error "unhashable type: 'Template'"
_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2)
class OfflineTestCaseMixin(object):
template_name = "test_compressor_offline.html"
verbosity = 0
# Change this for each test class
templates_dir = ""
expected_hash = ""
# Engines to test
if _TEST_JINJA2:
engines = ("django", "jinja2")
else:
engines = ("django",)
def setUp(self):
self._old_compress = settings.COMPRESS_ENABLED
self._old_compress_offline = settings.COMPRESS_OFFLINE
self._old_template_dirs = settings.TEMPLATE_DIRS
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
self.log = StringIO()
# Reset template dirs, because it enables us to force compress to
# consider only a specific directory (helps us make true,
# independant unit tests).
# Specify both Jinja2 and Django template locations. When the wrong engine
# is used to parse a template, the TemplateSyntaxError will cause the
# template to be skipped over.
django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir)
jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir)
settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir)
# Enable offline compress
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
if "django" in self.engines:
self.template_path = os.path.join(django_template_dir, self.template_name)
with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file:
self.template = Template(file.read())
self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT
if "jinja2" in self.engines:
# Setup Jinja2 settings.
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env()
jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
self.template_jinja2 = jinja2_env.from_string(file.read())
def tearDown(self):
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment
settings.COMPRESS_ENABLED = self._old_compress
settings.COMPRESS_OFFLINE = self._old_compress_offline
settings.TEMPLATE_DIRS = self._old_template_dirs
manifest_path = os.path.join('CACHE', 'manifest.json')
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
def _render_template(self, engine):
if engine == "django":
return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
elif engine == "jinja2":
return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
else:
return None
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(1, count)
self.assertEqual([
'' % (self.expected_hash, ),
], result)
rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
def test_offline(self):
for engine in self.engines:
self._test_offline(engine=engine)
def _get_jinja2_env(self):
import jinja2
import jinja2.ext
from compressor.offline.jinja2 import url_for, SpacelessExtension
from compressor.contrib.jinja2ext import CompressorExtension
# Extensions needed for the test cases only.
extensions = [
CompressorExtension,
SpacelessExtension,
jinja2.ext.with_,
jinja2.ext.do,
]
loader = self._get_jinja2_loader()
env = jinja2.Environment(extensions=extensions, loader=loader)
env.globals['url_for'] = url_for
return env
def _get_jinja2_loader(self):
import jinja2
loader = jinja2.FileSystemLoader(settings.TEMPLATE_DIRS, encoding=settings.FILE_CHARSET)
return loader
class OfflineGenerationSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_duplicate"
# We don't need to test multiples engines here.
engines = ("django",)
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
# Only one block compressed, the second identical one was skipped.
self.assertEqual(1, count)
# Only 1 ',
], result)
rendered_template = self._render_template(engine)
# But rendering the template returns both (identical) scripts.
self.assertEqual(rendered_template, "".join(result * 2) + "\n")
class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super"
expected_hash = "7c02d201f69d"
# Block.super not supported for Jinja2 yet.
engines = ("django",)
class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super_multiple"
expected_hash = "f8891c416981"
# Block.super not supported for Jinja2 yet.
engines = ("django",)
class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super_multiple_cached"
expected_hash = "2f6ef61c488e"
# Block.super not supported for Jinja2 yet.
engines = ("django",)
def setUp(self):
self._old_template_loaders = settings.TEMPLATE_LOADERS
settings.TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).setUp()
def tearDown(self):
super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).tearDown()
settings.TEMPLATE_LOADERS = self._old_template_loaders
class OfflineGenerationBlockSuperTestCaseWithExtraContent(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super_extra"
# Block.super not supported for Jinja2 yet.
engines = ("django",)
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(2, count)
self.assertEqual([
'',
''
], result)
rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
class OfflineGenerationConditionTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_condition"
expected_hash = "4e3758d50224"
def setUp(self):
self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.COMPRESS_OFFLINE_CONTEXT = {
'condition': 'red',
}
super(OfflineGenerationConditionTestCase, self).setUp()
def tearDown(self):
self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
super(OfflineGenerationConditionTestCase, self).tearDown()
class OfflineGenerationTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_templatetag"
expected_hash = "a27e1d3a619a"
class OfflineGenerationStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_static_templatetag"
expected_hash = "dfa2bb387fa8"
class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
templates_dir = "test_with_context"
expected_hash = "5838e2fd66af"
def setUp(self):
self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.COMPRESS_OFFLINE_CONTEXT = {
'content': 'OK!',
}
super(OfflineGenerationTestCaseWithContext, self).setUp()
def tearDown(self):
settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
super(OfflineGenerationTestCaseWithContext, self).tearDown()
class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
templates_dir = "test_error_handling"
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
if engine == "django":
self.assertEqual(2, count)
else:
# Because we use env.parse in Jinja2Parser, the engine does not
# actually load the "extends" and "includes" templates, and so
# it is unable to detect that they are missing. So all the "compress"
# nodes are processed correctly.
self.assertEqual(4, count)
self.assertEqual(engine, "jinja2")
self.assertIn('', result)
self.assertIn('', result)
self.assertIn('', result)
self.assertIn('', result)
class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_error_handling'
def setUp(self):
self._old_compress_precompilers = settings.COMPRESS_PRECOMPILERS
settings.COMPRESS_PRECOMPILERS = (('text/coffeescript', 'non-existing-binary'),)
super(OfflineGenerationTestCaseWithError, self).setUp()
def _test_offline(self, engine):
"""
Test that a CommandError is raised with DEBUG being False as well as
True, as otherwise errors in configuration will never show in
production.
"""
self._old_debug = settings.DEBUG
try:
settings.DEBUG = True
self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
settings.DEBUG = False
self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
finally:
settings.DEBUG = self._old_debug
def tearDown(self):
settings.COMPRESS_PRECOMPILERS = self._old_compress_precompilers
super(OfflineGenerationTestCaseWithError, self).tearDown()
class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "basic"
expected_hash = "f5e179b8eca4"
def test_rendering_without_manifest_raises_exception(self):
# flush cached manifest
flush_offline_manifest()
self.assertRaises(OfflineGenerationError,
self.template.render, Context({}))
@unittest.skipIf(not _TEST_JINJA2, "No Jinja2 testing")
def test_rendering_without_manifest_raises_exception_jinja2(self):
# flush cached manifest
flush_offline_manifest()
self.assertRaises(OfflineGenerationError,
self.template_jinja2.render, {})
def _test_deleting_manifest_does_not_affect_rendering(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
get_offline_manifest()
manifest_path = os.path.join('CACHE', 'manifest.json')
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
self.assertEqual(1, count)
self.assertEqual([
'' % (self.expected_hash, ),
], result)
rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
def test_deleting_manifest_does_not_affect_rendering(self):
for engine in self.engines:
self._test_deleting_manifest_does_not_affect_rendering(engine)
def test_requires_model_validation(self):
self.assertFalse(CompressCommand.requires_model_validation)
def test_get_loaders(self):
old_loaders = settings.TEMPLATE_LOADERS
settings.TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
try:
from django.template.loaders.filesystem import Loader as FileSystemLoader
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
except ImportError:
pass
else:
loaders = CompressCommand().get_loaders()
self.assertTrue(isinstance(loaders[0], FileSystemLoader))
self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
finally:
settings.TEMPLATE_LOADERS = old_loaders
class OfflineGenerationBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
template_names = ["base.html", "base2.html", "test_compressor_offline.html"]
templates_dir = 'test_block_super_base_compressed'
expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f8891c416981']
# Block.super not supported for Jinja2 yet.
engines = ("django",)
def setUp(self):
super(OfflineGenerationBlockSuperBaseCompressed, self).setUp()
self.template_paths = []
self.templates = []
for template_name in self.template_names:
template_path = os.path.join(settings.TEMPLATE_DIRS[0], template_name)
self.template_paths.append(template_path)
with io.open(template_path, encoding=settings.FILE_CHARSET) as file:
template = Template(file.read())
self.templates.append(template)
def _render_template(self, template, engine):
if engine == "django":
return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
elif engine == "jinja2":
return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
else:
return None
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(len(self.expected_hash), count)
for expected_hash, template in zip(self.expected_hash, self.templates):
expected_output = '' % (expected_hash, )
self.assertIn(expected_output, result)
rendered_template = self._render_template(template, engine)
self.assertEqual(rendered_template, expected_output + '\n')
class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_inline_non_ascii"
def setUp(self):
self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.COMPRESS_OFFLINE_CONTEXT = {
'test_non_ascii_value': '\u2014',
}
super(OfflineGenerationInlineNonAsciiTestCase, self).setUp()
def tearDown(self):
self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
super(OfflineGenerationInlineNonAsciiTestCase, self).tearDown()
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_complex"
def setUp(self):
self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.COMPRESS_OFFLINE_CONTEXT = {
'condition': 'OK!',
# Django templating does not allow definition of tuples in the
# templates. Make sure this is same as test_templates_jinja2/test_complex.
'my_names': ("js/one.js", "js/nonasc.js"),
}
super(OfflineGenerationComplexTestCase, self).setUp()
def tearDown(self):
self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
super(OfflineGenerationComplexTestCase, self).tearDown()
def _test_offline(self, engine):
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(3, count)
self.assertEqual([
'',
'',
'',
], result)
rendered_template = self._render_template(engine)
result = (result[0], result[2])
self.assertEqual(rendered_template, "".join(result) + "\n")
# Coffin does not work on Python 3.2+ due to:
# The line at coffin/template/__init__.py:15
# from library import *
# causing 'ImportError: No module named library'.
# It seems there is no evidence nor indicated support for Python 3+.
@unittest.skipIf(sys.version_info >= (3, 2),
"Coffin does not support 3.2+")
class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_coffin"
expected_hash = "32c8281e3346"
engines = ("jinja2",)
def _get_jinja2_env(self):
import jinja2
from coffin.common import env
from compressor.contrib.jinja2ext import CompressorExtension
# Could have used the env.add_extension method, but it's only available
# in Jinja2 v2.5
new_env = jinja2.Environment(extensions=[CompressorExtension])
env.extensions.update(new_env.extensions)
return env
# Jingo does not work when using Python 3.2 due to the use of Unicode string
# prefix (and possibly other stuff), but it actually works when using Python 3.3
# since it tolerates the use of the Unicode string prefix. Python 3.3 support
# is also evident in its tox.ini file.
@unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3),
"Jingo does not support 3.2")
class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_jingo"
expected_hash = "61ec584468eb"
engines = ("jinja2",)
def _get_jinja2_env(self):
import jinja2
import jinja2.ext
from jingo import env
from compressor.contrib.jinja2ext import CompressorExtension
from compressor.offline.jinja2 import SpacelessExtension, url_for
# Could have used the env.add_extension method, but it's only available
# in Jinja2 v2.5
new_env = jinja2.Environment(extensions=[CompressorExtension, SpacelessExtension, jinja2.ext.with_])
env.extensions.update(new_env.extensions)
env.globals['url_for'] = url_for
return env