Moved tests out of the app to stay sane and changed a few things. E.g. the inclusion of JavaScript files now happens without charset (as they are deprecated in HTML5).
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
build
|
||||
compressor/tests/media/CACHE
|
||||
compressor/tests/media/custom
|
||||
compressor/tests/media/js/066cd253eada.js
|
||||
tests/media/CACHE
|
||||
tests/media/custom
|
||||
tests/media/js/066cd253eada.js
|
||||
dist
|
||||
MANIFEST
|
||||
*.pyc
|
||||
|
||||
@@ -2,4 +2,4 @@ include AUTHORS
|
||||
include README.rst
|
||||
include LICENSE
|
||||
recursive-include compressor/templates/compressor *.html
|
||||
recursive-include compressor/tests/media *.js *.css *.png
|
||||
recursive-include tests *.js *.css *.png *.py
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
import codecs
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.template.loader import render_to_string
|
||||
@@ -10,10 +12,10 @@ from compressor.exceptions import CompressorError, UncompressableFileError
|
||||
from compressor.filters import CompilerFilter
|
||||
from compressor.storage import default_storage
|
||||
from compressor.utils import get_class, staticfiles
|
||||
from compressor.utils.decorators import cached_property
|
||||
from compressor.utils.decorators import cached_property, memoize
|
||||
|
||||
# Some constants for nicer handling.
|
||||
SOURCE_HUNK, SOURCE_FILE = 1, 2
|
||||
SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
|
||||
METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
|
||||
|
||||
|
||||
@@ -27,6 +29,7 @@ class Compressor(object):
|
||||
def __init__(self, content=None, output_prefix="compressed"):
|
||||
self.content = content or ""
|
||||
self.output_prefix = output_prefix
|
||||
self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
|
||||
self.charset = settings.DEFAULT_CHARSET
|
||||
self.storage = default_storage
|
||||
self.split_content = []
|
||||
@@ -47,17 +50,21 @@ class Compressor(object):
|
||||
except AttributeError:
|
||||
base_url = settings.COMPRESS_URL
|
||||
if not url.startswith(base_url):
|
||||
raise UncompressableFileError(
|
||||
"'%s' isn't accesible via COMPRESS_URL ('%s') and can't be"
|
||||
" compressed" % (url, base_url))
|
||||
raise UncompressableFileError("'%s' isn't accesible via "
|
||||
"COMPRESS_URL ('%s') and can't be "
|
||||
"compressed" % (url, base_url))
|
||||
basename = url.replace(base_url, "", 1)
|
||||
# drop the querystring, which is used for non-compressed cache-busting.
|
||||
return basename.split("?", 1)[0]
|
||||
|
||||
def get_filepath(self, content):
|
||||
filename = "%s.%s" % (get_hexdigest(content, 12), self.type)
|
||||
return os.path.join(self.output_dir, self.output_prefix, filename)
|
||||
|
||||
def get_filename(self, basename):
|
||||
# first try to find it with staticfiles (in debug mode)
|
||||
filename = None
|
||||
if settings.DEBUG and self.finders:
|
||||
if self.finders:
|
||||
filename = self.finders.find(basename)
|
||||
# secondly try finding the file in the root
|
||||
elif self.storage.exists(basename):
|
||||
@@ -66,9 +73,17 @@ class Compressor(object):
|
||||
return filename
|
||||
# or just raise an exception as the last resort
|
||||
raise UncompressableFileError(
|
||||
"'%s' could not be found in the COMPRESS_ROOT '%s'%s" % (
|
||||
basename, settings.COMPRESS_ROOT,
|
||||
self.finders and " or with staticfiles." or "."))
|
||||
"'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
|
||||
(basename, settings.COMPRESS_ROOT,
|
||||
self.finders and " or with staticfiles." or "."))
|
||||
|
||||
def get_filecontent(self, filename, charset):
|
||||
with codecs.open(filename, 'rb', charset) as fd:
|
||||
try:
|
||||
return fd.read()
|
||||
except IOError, e:
|
||||
raise UncompressableFileError("IOError while processing "
|
||||
"'%s': %s" % (filename, e))
|
||||
|
||||
@cached_property
|
||||
def parser(self):
|
||||
@@ -89,36 +104,70 @@ class Compressor(object):
|
||||
return get_hexdigest(''.join(
|
||||
[self.content] + self.mtimes).encode(self.charset), 12)
|
||||
|
||||
@cached_property
|
||||
def hunks(self):
|
||||
for kind, value, basename, elem in self.split_contents():
|
||||
if kind == SOURCE_HUNK:
|
||||
content = self.filter(value, METHOD_INPUT,
|
||||
elem=elem, kind=kind, basename=basename)
|
||||
yield smart_unicode(content)
|
||||
elif kind == SOURCE_FILE:
|
||||
content = ""
|
||||
fd = open(value, 'rb')
|
||||
try:
|
||||
content = fd.read()
|
||||
except IOError, e:
|
||||
raise UncompressableFileError(
|
||||
"IOError while processing '%s': %s" % (value, e))
|
||||
finally:
|
||||
fd.close()
|
||||
content = self.filter(content, METHOD_INPUT,
|
||||
filename=value, basename=basename, elem=elem, kind=kind)
|
||||
attribs = self.parser.elem_attribs(elem)
|
||||
charset = attribs.get("charset", self.charset)
|
||||
yield smart_unicode(content, charset.lower())
|
||||
@memoize
|
||||
def hunks(self, mode='file'):
|
||||
"""
|
||||
The heart of content parsing, iterates of the
|
||||
list of split contents and looks at its kind
|
||||
to decide what to do with it. Should yield a
|
||||
bunch of precompiled and/or rendered hunks.
|
||||
"""
|
||||
enabled = settings.COMPRESS_ENABLED
|
||||
|
||||
@cached_property
|
||||
def concat(self):
|
||||
return '\n'.join((hunk.encode(self.charset) for hunk in self.hunks))
|
||||
for kind, value, basename, elem in self.split_contents():
|
||||
precompiled = False
|
||||
attribs = self.parser.elem_attribs(elem)
|
||||
charset = attribs.get("charset", self.charset)
|
||||
options = {
|
||||
'method': METHOD_INPUT,
|
||||
'elem': elem,
|
||||
'kind': kind,
|
||||
'basename': basename,
|
||||
}
|
||||
|
||||
if kind == SOURCE_FILE:
|
||||
options = dict(options, filename=value)
|
||||
value = self.get_filecontent(value, charset)
|
||||
|
||||
if self.all_mimetypes:
|
||||
precompiled, value = self.precompile(value, **options)
|
||||
|
||||
if enabled:
|
||||
value = self.filter(value, **options)
|
||||
yield mode, smart_unicode(value, charset.lower())
|
||||
else:
|
||||
if precompiled:
|
||||
value = self.handle_output(kind, value, forced=True)
|
||||
yield "verbatim", smart_unicode(value, charset.lower())
|
||||
else:
|
||||
yield mode, self.parser.elem_str(elem)
|
||||
|
||||
@memoize
|
||||
def filtered_output(self, content):
|
||||
"""
|
||||
Passes the concatenated content to the 'output' methods
|
||||
of the compressor filters.
|
||||
"""
|
||||
return self.filter(content, method=METHOD_OUTPUT)
|
||||
|
||||
@memoize
|
||||
def filtered_input(self, mode='file'):
|
||||
"""
|
||||
Passes each hunk (file or code) to the 'input' methods
|
||||
of the compressor filters.
|
||||
"""
|
||||
verbatim_content = []
|
||||
rendered_content = []
|
||||
for mode, hunk in self.hunks(mode):
|
||||
if mode == 'verbatim':
|
||||
verbatim_content.append(hunk)
|
||||
else:
|
||||
rendered_content.append(hunk)
|
||||
return verbatim_content, rendered_content
|
||||
|
||||
def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
|
||||
if not kind:
|
||||
return content
|
||||
return False, content
|
||||
attrs = self.parser.elem_attribs(elem)
|
||||
mimetype = attrs.get("type", None)
|
||||
if mimetype:
|
||||
@@ -129,15 +178,11 @@ class Compressor(object):
|
||||
"COMPRESS_PRECOMPILERS setting for "
|
||||
"mimetype '%s'." % mimetype)
|
||||
else:
|
||||
return CompilerFilter(content, filter_type=self.type,
|
||||
return True, CompilerFilter(content, filter_type=self.type,
|
||||
command=command, filename=filename).input(**kwargs)
|
||||
return content
|
||||
return False, content
|
||||
|
||||
def filter(self, content, method, **kwargs):
|
||||
# run compiler
|
||||
if method == METHOD_INPUT:
|
||||
content = self.precompile(content, **kwargs)
|
||||
|
||||
for filter_cls in self.cached_filters:
|
||||
filter_func = getattr(
|
||||
filter_cls(content, filter_type=self.type), method)
|
||||
@@ -148,34 +193,28 @@ class Compressor(object):
|
||||
pass
|
||||
return content
|
||||
|
||||
@cached_property
|
||||
def combined(self):
|
||||
return self.filter(self.concat, method=METHOD_OUTPUT)
|
||||
|
||||
def filepath(self, content):
|
||||
return os.path.join(settings.COMPRESS_OUTPUT_DIR.strip(os.sep),
|
||||
self.output_prefix, "%s.%s" % (get_hexdigest(content, 12), self.type))
|
||||
|
||||
def output(self, mode='file', forced=False):
|
||||
"""
|
||||
The general output method, override in subclass if you need to do
|
||||
any custom modification. Calls other mode specific methods or simply
|
||||
returns the content directly.
|
||||
"""
|
||||
# First check whether we should do the full compression,
|
||||
# including precompilation (or if it's forced)
|
||||
if settings.COMPRESS_ENABLED or forced:
|
||||
content = self.combined
|
||||
elif settings.COMPRESS_PRECOMPILERS:
|
||||
# or concatting it, if pre-compilation is enabled
|
||||
content = self.concat
|
||||
else:
|
||||
# or just doing nothing, when neither
|
||||
# compression nor compilation is enabled
|
||||
return self.content
|
||||
# Shortcurcuit in case the content is empty.
|
||||
if not content:
|
||||
verbatim_content, rendered_content = self.filtered_input(mode)
|
||||
if not verbatim_content and not rendered_content:
|
||||
return ''
|
||||
|
||||
if settings.COMPRESS_ENABLED or forced:
|
||||
filtered_content = self.filtered_output(
|
||||
'\n'.join((c.encode(self.charset) for c in rendered_content)))
|
||||
finished_content = self.handle_output(mode, filtered_content, forced)
|
||||
verbatim_content.append(finished_content)
|
||||
|
||||
if verbatim_content:
|
||||
return '\n'.join(verbatim_content)
|
||||
|
||||
return self.content
|
||||
|
||||
def handle_output(self, mode, content, forced):
|
||||
# Then check for the appropriate output method and call it
|
||||
output_func = getattr(self, "output_%s" % mode, None)
|
||||
if callable(output_func):
|
||||
@@ -189,7 +228,7 @@ class Compressor(object):
|
||||
The output method that saves the content to a file and renders
|
||||
the appropriate template with the file's URL.
|
||||
"""
|
||||
new_filepath = self.filepath(content)
|
||||
new_filepath = self.get_filepath(content)
|
||||
if not self.storage.exists(new_filepath) or forced:
|
||||
self.storage.save(new_filepath, ContentFile(content))
|
||||
url = self.storage.url(new_filepath)
|
||||
|
||||
@@ -10,7 +10,7 @@ class CssCompressor(Compressor):
|
||||
def __init__(self, content=None, output_prefix="css"):
|
||||
super(CssCompressor, self).__init__(content, output_prefix)
|
||||
self.filters = list(settings.COMPRESS_CSS_FILTERS)
|
||||
self.type = 'css'
|
||||
self.type = output_prefix
|
||||
|
||||
def split_contents(self):
|
||||
if self.split_content:
|
||||
@@ -21,13 +21,9 @@ class CssCompressor(Compressor):
|
||||
elem_name = self.parser.elem_name(elem)
|
||||
elem_attribs = self.parser.elem_attribs(elem)
|
||||
if elem_name == 'link' and elem_attribs['rel'] == 'stylesheet':
|
||||
try:
|
||||
basename = self.get_basename(elem_attribs['href'])
|
||||
filename = self.get_filename(basename)
|
||||
data = (SOURCE_FILE, filename, basename, elem)
|
||||
except UncompressableFileError:
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
basename = self.get_basename(elem_attribs['href'])
|
||||
filename = self.get_filename(basename)
|
||||
data = (SOURCE_FILE, filename, basename, elem)
|
||||
elif elem_name == 'style':
|
||||
data = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
|
||||
if data:
|
||||
@@ -38,15 +34,15 @@ class CssCompressor(Compressor):
|
||||
if self.media_nodes and self.media_nodes[-1][0] == media:
|
||||
self.media_nodes[-1][1].split_content.append(data)
|
||||
else:
|
||||
node = CssCompressor(str(elem))
|
||||
node = CssCompressor(self.parser.elem_str(elem))
|
||||
node.split_content.append(data)
|
||||
self.media_nodes.append((media, node))
|
||||
return self.split_content
|
||||
|
||||
def output(self, *args, **kwargs):
|
||||
# Populate self.split_content
|
||||
if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or
|
||||
kwargs.get('forced', False)):
|
||||
# Populate self.split_content
|
||||
self.split_contents()
|
||||
if hasattr(self, 'media_nodes'):
|
||||
ret = []
|
||||
|
||||
@@ -10,7 +10,7 @@ class JsCompressor(Compressor):
|
||||
def __init__(self, content=None, output_prefix="js"):
|
||||
super(JsCompressor, self).__init__(content, output_prefix)
|
||||
self.filters = list(settings.COMPRESS_JS_FILTERS)
|
||||
self.type = 'js'
|
||||
self.type = output_prefix
|
||||
|
||||
def split_contents(self):
|
||||
if self.split_content:
|
||||
@@ -18,14 +18,10 @@ class JsCompressor(Compressor):
|
||||
for elem in self.parser.js_elems():
|
||||
attribs = self.parser.elem_attribs(elem)
|
||||
if 'src' in attribs:
|
||||
try:
|
||||
basename = self.get_basename(attribs['src'])
|
||||
filename = self.get_filename(basename)
|
||||
self.split_content.append(
|
||||
(SOURCE_FILE, filename, basename, elem))
|
||||
except UncompressableFileError:
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
basename = self.get_basename(attribs['src'])
|
||||
filename = self.get_filename(basename)
|
||||
content = (SOURCE_FILE, filename, basename, elem)
|
||||
self.split_content.append(content)
|
||||
else:
|
||||
content = self.parser.elem_content(elem)
|
||||
self.split_content.append((SOURCE_HUNK, content, None, elem))
|
||||
|
||||
@@ -1 +1 @@
|
||||
<style type="text/css"{% if media %} media="{{ media }}"{% endif %}>{{ content|safe }}</style>
|
||||
<style type="text/css"{% if media %} media="{{ media }}"{% endif %}>{{ content|safe }}</style>
|
||||
@@ -1 +1 @@
|
||||
<script type="text/javascript" src="{{ url }}" charset="utf-8"></script>
|
||||
<script type="text/javascript" src="{{ url }}"></script>
|
||||
@@ -1,609 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from unittest2 import skipIf
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
try:
|
||||
import lxml
|
||||
except ImportError:
|
||||
lxml = None
|
||||
|
||||
try:
|
||||
import html5lib
|
||||
except ImportError:
|
||||
html5lib = None
|
||||
|
||||
try:
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
except ImportError:
|
||||
BeautifulSoup = None
|
||||
|
||||
from django.core.cache.backends import locmem
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.template import Template, Context, TemplateSyntaxError
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor import base
|
||||
from compressor.base import SOURCE_HUNK, SOURCE_FILE
|
||||
from compressor.cache import get_hashed_mtime, get_hexdigest
|
||||
from compressor.conf import settings
|
||||
from compressor.css import CssCompressor
|
||||
from compressor.js import JsCompressor
|
||||
from compressor.management.commands.compress import Command as CompressCommand
|
||||
from compressor.utils import find_command
|
||||
from compressor.filters.base import CompilerFilter
|
||||
|
||||
def css_tag(href, **kwargs):
|
||||
rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
|
||||
template = u'<link rel="stylesheet" href="%s" type="text/css" %s/>'
|
||||
return template % (href, rendered_attrs)
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
class CompressorTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_PRECOMPILERS = {}
|
||||
settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8">
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
self.js = """
|
||||
<script src="/media/js/one.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
|
||||
"""
|
||||
self.js_node = JsCompressor(self.js)
|
||||
|
||||
def test_css_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8" />'),
|
||||
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" />'),
|
||||
]
|
||||
split = self.css_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_css_hunks(self):
|
||||
out = ['body { background:#990; }', u'p { border:5px solid green;}', 'body { color:#fff; }']
|
||||
self.assertEqual(out, list(self.css_node.hunks))
|
||||
|
||||
def test_css_output(self):
|
||||
out = u'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
|
||||
self.assertEqual(out, self.css_node.combined)
|
||||
|
||||
def test_css_mtimes(self):
|
||||
is_date = re.compile(r'^\d{10}[\.\d]+$')
|
||||
for date in self.css_node.mtimes:
|
||||
self.assertTrue(is_date.match(str(float(date))),
|
||||
"mtimes is returning something that doesn't look like a date: %s" % date)
|
||||
|
||||
def test_css_return_if_off(self):
|
||||
settings.COMPRESS_ENABLED = False
|
||||
self.assertEqual(self.css, self.css_node.output())
|
||||
|
||||
def test_cachekey(self):
|
||||
is_cachekey = re.compile(r'\w{12}')
|
||||
self.assertTrue(is_cachekey.match(self.css_node.cachekey),
|
||||
"cachekey is returning something that doesn't look like r'\w{12}'")
|
||||
|
||||
def test_css_hash(self):
|
||||
self.assertEqual('c618e6846d04', get_hexdigest(self.css, 12))
|
||||
|
||||
def test_css_return_if_on(self):
|
||||
output = css_tag('/media/CACHE/css/e41ba2cc6982.css')
|
||||
self.assertEqual(output, self.css_node.output().strip())
|
||||
|
||||
def test_js_split(self):
|
||||
out = [(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', '<script src="/media/js/one.js" type="text/javascript" charset="utf-8"></script>'),
|
||||
(SOURCE_HUNK, u'obj.value = "value";', None, '<script type="text/javascript" charset="utf-8">obj.value = "value";</script>')
|
||||
]
|
||||
split = self.js_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_js_hunks(self):
|
||||
out = ['obj = {};', u'obj.value = "value";']
|
||||
self.assertEqual(out, list(self.js_node.hunks))
|
||||
|
||||
def test_js_concat(self):
|
||||
out = u'obj = {};\nobj.value = "value";'
|
||||
self.assertEqual(out, self.js_node.concat)
|
||||
|
||||
def test_js_output(self):
|
||||
out = u'obj={};obj.value="value";'
|
||||
self.assertEqual(out, self.js_node.combined)
|
||||
|
||||
def test_js_return_if_off(self):
|
||||
try:
|
||||
enabled = settings.COMPRESS_ENABLED
|
||||
precompilers = settings.COMPRESS_PRECOMPILERS
|
||||
settings.COMPRESS_ENABLED = False
|
||||
settings.COMPRESS_PRECOMPILERS = {}
|
||||
self.assertEqual(self.js, self.js_node.output())
|
||||
finally:
|
||||
settings.COMPRESS_ENABLED = enabled
|
||||
settings.COMPRESS_PRECOMPILERS = precompilers
|
||||
|
||||
def test_js_return_if_on(self):
|
||||
output = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js" charset="utf-8"></script>'
|
||||
self.assertEqual(output, self.js_node.output())
|
||||
|
||||
def test_custom_output_dir(self):
|
||||
try:
|
||||
old_output_dir = settings.COMPRESS_OUTPUT_DIR
|
||||
settings.COMPRESS_OUTPUT_DIR = 'custom'
|
||||
output = u'<script type="text/javascript" src="/media/custom/js/066cd253eada.js" charset="utf-8"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
settings.COMPRESS_OUTPUT_DIR = ''
|
||||
output = u'<script type="text/javascript" src="/media/js/066cd253eada.js" charset="utf-8"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
settings.COMPRESS_OUTPUT_DIR = '/custom/nested/'
|
||||
output = u'<script type="text/javascript" src="/media/custom/nested/js/066cd253eada.js" charset="utf-8"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
finally:
|
||||
settings.COMPRESS_OUTPUT_DIR = old_output_dir
|
||||
|
||||
|
||||
class ParserTestCase(object):
|
||||
|
||||
def setUp(self):
|
||||
self.old_parser = settings.COMPRESS_PARSER
|
||||
settings.COMPRESS_PARSER = self.parser_cls
|
||||
super(ParserTestCase, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_PARSER = self.old_parser
|
||||
|
||||
|
||||
class LxmlParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.LxmlParser'
|
||||
LxmlParserTests = skipIf(lxml is None, 'lxml not found')(LxmlParserTests)
|
||||
|
||||
|
||||
class Html5LibParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.Html5LibParser'
|
||||
|
||||
def test_css_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link charset="utf-8" href="/media/css/one.css" rel="stylesheet" type="text/css">'),
|
||||
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link charset="utf-8" href="/media/css/two.css" rel="stylesheet" type="text/css">'),
|
||||
]
|
||||
split = self.css_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_js_split(self):
|
||||
out = [(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', u'<script charset="utf-8" src="/media/js/one.js" type="text/javascript"></script>'),
|
||||
(SOURCE_HUNK, u'obj.value = "value";', None, u'<script charset="utf-8" type="text/javascript">obj.value = "value";</script>')
|
||||
]
|
||||
split = self.js_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
Html5LibParserTests = skipIf(
|
||||
html5lib is None, 'html5lib not found')(Html5LibParserTests)
|
||||
|
||||
|
||||
class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.BeautifulSoupParser'
|
||||
|
||||
BeautifulSoupParserTests = skipIf(
|
||||
BeautifulSoup is None, 'BeautifulSoup not found')(BeautifulSoupParserTests)
|
||||
|
||||
|
||||
class HtmlParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.HtmlParser'
|
||||
|
||||
|
||||
class CssAbsolutizingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/url/url1.css" type="text/css" charset="utf-8">
|
||||
<link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css" charset="utf-8">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_css_absolute_filter(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'http://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_https(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_relative_path(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_hunks(self):
|
||||
hash_dict = {
|
||||
'hash1': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/url1.css')),
|
||||
'hash2': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/2/url2.css')),
|
||||
}
|
||||
out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict,
|
||||
u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict]
|
||||
self.assertEqual(out, list(self.css_node.hunks))
|
||||
|
||||
|
||||
class CssDataUriTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_CSS_FILTERS = [
|
||||
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
'compressor.filters.datauri.CssDataUriFilter',
|
||||
]
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/datauri.css" type="text/css" charset="utf-8">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_data_uris(self):
|
||||
datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/datauri.css'))
|
||||
out = [u'.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }\n.python { background-image: url("/media/img/python.png?%s"); }\n.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }\n' % datauri_hash]
|
||||
self.assertEqual(out, list(self.css_node.hunks))
|
||||
|
||||
|
||||
class CssMediaTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/one.css" type="text/css" media="screen" charset="utf-8">
|
||||
<style type="text/css" media="print">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" media="all">
|
||||
<style type="text/css">h1 { border:5px solid green;}</style>
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_css_output(self):
|
||||
links = BeautifulSoup(self.css_node.output()).findAll('link')
|
||||
media = [u'screen', u'print', u'all', None]
|
||||
self.assertEqual(len(links), 4)
|
||||
self.assertEqual(media, [l.get('media', None) for l in links])
|
||||
|
||||
def test_avoid_reordering_css(self):
|
||||
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
|
||||
node = CssCompressor(css)
|
||||
media = [u'screen', u'print', u'all', None, u'print']
|
||||
links = BeautifulSoup(node.output()).findAll('link')
|
||||
self.assertEqual(media, [l.get('media', None) for l in links])
|
||||
|
||||
|
||||
class CssMinTestCase(TestCase):
|
||||
def test_cssmin_filter(self):
|
||||
from compressor.filters.cssmin import CSSMinFilter
|
||||
content = """p {
|
||||
|
||||
|
||||
background: rgb(51,102,153) url('../../images/image.gif');
|
||||
|
||||
|
||||
}
|
||||
"""
|
||||
output = "p{background:#369 url('../../images/image.gif')}"
|
||||
self.assertEqual(output, CSSMinFilter(content).output())
|
||||
|
||||
def render(template_string, context_dict=None):
|
||||
"""A shortcut for testing template output."""
|
||||
if context_dict is None:
|
||||
context_dict = {}
|
||||
|
||||
c = Context(context_dict)
|
||||
t = Template(template_string)
|
||||
return t.render(c).strip()
|
||||
|
||||
|
||||
class TemplatetagTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
|
||||
def test_empty_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}{% block js %}
|
||||
{% endblock %}{% endcompress %}"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
self.assertEqual(u'', render(template, context))
|
||||
|
||||
def test_css_tag(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8">
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8">
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = css_tag("/media/CACHE/css/e41ba2cc6982.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
def test_nonascii_css_tag(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/nonasc.css" type="text/css" charset="utf-8">
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = css_tag("/media/CACHE/css/799f6defe43c.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
def test_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js" charset="utf-8"></script>'
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
def test_nonascii_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/nonasc.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/e214fe629b28.js" charset="utf-8"></script>'
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
def test_nonascii_latin1_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/nonasc_latin1.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/f1be5a5de243.js" charset="utf-8"></script>'
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
def test_compress_tag_with_illegal_arguments(self):
|
||||
template = u"""{% load compress %}{% compress pony %}
|
||||
<script type="pony/application">unicorn</script>
|
||||
{% endcompress %}"""
|
||||
self.assertRaises(TemplateSyntaxError, render, template, {})
|
||||
|
||||
def test_debug_toggle(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
class MockDebugRequest(object):
|
||||
GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'}
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL, 'request': MockDebugRequest()}
|
||||
out = u"""<script src="/media/js/one.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>"""
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
class StorageTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self._storage = base.default_storage
|
||||
base.default_storage = get_storage_class('compressor.storage.GzipCompressorFileStorage')()
|
||||
settings.COMPRESS_ENABLED = True
|
||||
|
||||
def tearDown(self):
|
||||
base.default_storage = self._storage
|
||||
|
||||
def test_css_tag_with_storage(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8">
|
||||
<style type="text/css">p { border:5px solid white;}</style>
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8">
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = { 'MEDIA_URL': settings.COMPRESS_URL }
|
||||
out = css_tag("/media/CACHE/css/1d4424458f88.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
|
||||
class VerboseTestCase(CompressorTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VerboseTestCase, self).setUp()
|
||||
settings.COMPRESS_VERBOSE = True
|
||||
|
||||
|
||||
class CacheBackendTestCase(CompressorTestCase):
|
||||
|
||||
def test_correct_backend(self):
|
||||
from compressor.cache import cache
|
||||
self.assertEqual(cache.__class__, locmem.CacheClass)
|
||||
|
||||
|
||||
class OfflineGenerationTestCase(TestCase):
|
||||
"""Uses templates/test_compressor_offline.html"""
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
self._old_compress = settings.COMPRESS_ENABLED
|
||||
self._old_compress_offline = settings.COMPRESS_OFFLINE
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_OFFLINE = True
|
||||
self.template_file = open(os.path.join(here, "templates/test_compressor_offline.html"))
|
||||
self.template = Template(self.template_file.read().decode(settings.FILE_CHARSET))
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_ENABLED = self._old_compress
|
||||
settings.COMPRESS_OFFLINE = self._old_compress_offline
|
||||
self.template_file.close()
|
||||
|
||||
def test_offline(self):
|
||||
count, result = CompressCommand().compress()
|
||||
self.assertEqual(5, count)
|
||||
self.assertEqual([
|
||||
css_tag('/media/CACHE/css/cd579b7deb7d.css')+'\n',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js" charset="utf-8"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js" charset="utf-8"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js" charset="utf-8"></script>',
|
||||
u'<link rel="stylesheet" href="/media/CACHE/css/67ed6aff7f7b.css" type="text/css" />\n',
|
||||
], result)
|
||||
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
|
||||
# key<->values ourselves?
|
||||
rendered_template = self.template.render(Context({})).replace("\n", "")
|
||||
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
|
||||
|
||||
def test_offline_with_context(self):
|
||||
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
|
||||
settings.COMPRESS_OFFLINE_CONTEXT = {
|
||||
'color': 'blue',
|
||||
}
|
||||
count, result = CompressCommand().compress()
|
||||
self.assertEqual(5, count)
|
||||
self.assertEqual([
|
||||
css_tag('/media/CACHE/css/ee62fbfd116a.css')+'\n',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js" charset="utf-8"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js" charset="utf-8"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js" charset="utf-8"></script>',
|
||||
u'<link rel="stylesheet" href="/media/CACHE/css/73e015f740c6.css" type="text/css" />\n',
|
||||
], result)
|
||||
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
|
||||
# key<->values ourselves?
|
||||
rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)).replace("\n", "")
|
||||
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
|
||||
settings.COMPRESS_OFFLINE_CONTEXT = self._old_offline_context
|
||||
|
||||
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 CssTidyTestCase(TestCase):
|
||||
def test_tidy(self):
|
||||
content = """
|
||||
/* Some comment */
|
||||
font,th,td,p{
|
||||
color: black;
|
||||
}
|
||||
"""
|
||||
from compressor.filters.csstidy import CSSTidyFilter
|
||||
self.assertEqual(
|
||||
"font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
|
||||
|
||||
CssTidyTestCase = skipIf(
|
||||
find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
|
||||
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY
|
||||
)(CssTidyTestCase)
|
||||
|
||||
|
||||
class CompassTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.old_debug = settings.DEBUG
|
||||
self.old_compress_css_filters = settings.COMPRESS_CSS_FILTERS
|
||||
self.old_compress_url = settings.COMPRESS_URL
|
||||
self.old_enabled = settings.COMPRESS_ENABLED
|
||||
settings.DEBUG = True
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_CSS_FILTERS = [
|
||||
'compressor.filters.compass.CompassFilter',
|
||||
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
]
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
|
||||
def tearDown(self):
|
||||
settings.DEBUG = self.old_debug
|
||||
settings.COMPRESS_URL = self.old_compress_url
|
||||
settings.COMPRESS_ENABLED = self.old_enabled
|
||||
settings.COMPRESS_CSS_FILTERS = self.old_compress_css_filters
|
||||
|
||||
def test_compass(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/screen.scss" type="text/css" charset="utf-8">
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/print.scss" type="text/css" charset="utf-8">
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = {'MEDIA_URL': settings.COMPRESS_URL}
|
||||
out = css_tag("/media/CACHE/css/8ff1cfd8787d.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
CompassTestCase = skipIf(
|
||||
find_command(settings.COMPRESS_COMPASS_BINARY) is None,
|
||||
'Compass binary %r not found' % settings.COMPRESS_COMPASS_BINARY
|
||||
)(CompassTestCase)
|
||||
|
||||
|
||||
class PrecompilerTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.this_dir = os.path.dirname(__file__)
|
||||
self.filename = os.path.join(self.this_dir, 'media/css/one.css')
|
||||
with open(self.filename) as f:
|
||||
self.content = f.read()
|
||||
self.test_precompiler = os.path.join(self.this_dir, 'precompiler.py')
|
||||
|
||||
def test_precompiler_infile_outfile(self):
|
||||
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
|
||||
self.assertEqual(u"body { color:#990; }", compiler.input())
|
||||
|
||||
def test_precompiler_infile_stdout(self):
|
||||
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_outfile(self):
|
||||
command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_stdout(self):
|
||||
command = '%s %s' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_stdout_filename(self):
|
||||
command = '%s %s' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
@@ -1,3 +1,29 @@
|
||||
import functools
|
||||
|
||||
class memoize(object):
|
||||
|
||||
def __init__ (self, func):
|
||||
self.func = func
|
||||
|
||||
def __call__ (self, *args, **kwargs):
|
||||
if (args, str(kwargs)) in self.__dict__:
|
||||
value = self.__dict__[args, str(kwargs)]
|
||||
else:
|
||||
value = self.func(*args, **kwargs)
|
||||
self.__dict__[args, str(kwargs)] = value
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return the function's docstring.
|
||||
"""
|
||||
return self.func.__doc__ or ''
|
||||
|
||||
def __get__(self, obj, objtype):
|
||||
"""
|
||||
Support instance methods.
|
||||
"""
|
||||
return functools.partial(self.__call__, obj)
|
||||
|
||||
|
||||
class cached_property(object):
|
||||
|
||||
2
setup.py
2
setup.py
@@ -109,7 +109,7 @@ setup(
|
||||
long_description = README,
|
||||
author = 'Jannis Leidel',
|
||||
author_email = 'jannis@leidel.info',
|
||||
packages = find_packages(),
|
||||
packages = find_packages(exclude=['tests', 'tests.*']),
|
||||
package_data = find_package_data('compressor', only_in_packages=False),
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
|
||||
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -14,7 +14,7 @@ if not settings.configured:
|
||||
DATABASE_ENGINE='sqlite3',
|
||||
INSTALLED_APPS=[
|
||||
'compressor',
|
||||
'compressor.tests',
|
||||
'tests',
|
||||
],
|
||||
MEDIA_URL = '/media/',
|
||||
MEDIA_ROOT = os.path.join(TEST_DIR, 'media'),
|
||||
@@ -30,14 +30,13 @@ from django.test.simple import run_tests
|
||||
def runtests(*test_args):
|
||||
if not test_args:
|
||||
test_args = ['tests']
|
||||
parent_dir = os.path.join(TEST_DIR, "..", "..")
|
||||
parent_dir = os.path.join(TEST_DIR, "..")
|
||||
sys.path.insert(0, parent_dir)
|
||||
cov = coverage.coverage(branch=True,
|
||||
include=[
|
||||
os.path.join(parent_dir, 'compressor', '*.py')
|
||||
],
|
||||
omit=[
|
||||
join(parent_dir, 'compressor', 'tests', '*.py'),
|
||||
join(parent_dir, 'compressor', 'utils', 'stringformat.py'),
|
||||
join(parent_dir, 'compressor', 'filters', 'jsmin', 'rjsmin.py'),
|
||||
join(parent_dir, 'compressor', 'filters', 'cssmin', 'cssmin.py'),
|
||||
6
tests/tests/__init__.py
Normal file
6
tests/tests/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .base import CompressorTestCase, CssMediaTestCase, VerboseTestCase, CacheBackendTestCase
|
||||
from .filters import CssTidyTestCase, CompassTestCase, PrecompilerTestCase, CssMinTestCase, CssAbsolutizingTestCase, CssDataUriTestCase
|
||||
from .offline import OfflineGenerationTestCase
|
||||
from .parsers import LxmlParserTests, Html5LibParserTests, BeautifulSoupParserTests, HtmlParserTests
|
||||
from .storages import StorageTestCase
|
||||
from .templatetags import TemplatetagTestCase
|
||||
169
tests/tests/base.py
Normal file
169
tests/tests/base.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
import re
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
from django.core.cache.backends import locmem
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor.base import SOURCE_HUNK, SOURCE_FILE
|
||||
from compressor.cache import get_hexdigest
|
||||
from compressor.conf import settings
|
||||
from compressor.css import CssCompressor
|
||||
from compressor.js import JsCompressor
|
||||
|
||||
|
||||
def css_tag(href, **kwargs):
|
||||
rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
|
||||
template = u'<link rel="stylesheet" href="%s" type="text/css" %s/>'
|
||||
return template % (href, rendered_attrs)
|
||||
|
||||
|
||||
test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
class CompressorTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_PRECOMPILERS = {}
|
||||
settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
|
||||
self.css = """\
|
||||
<link rel="stylesheet" href="/media/css/one.css" type="text/css" />
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="/media/css/two.css" type="text/css" />"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
self.js = """\
|
||||
<script src="/media/js/one.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">obj.value = "value";</script>"""
|
||||
self.js_node = JsCompressor(self.js)
|
||||
|
||||
def test_css_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" />'),
|
||||
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" />'),
|
||||
]
|
||||
split = self.css_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_css_hunks(self):
|
||||
out = ['body { background:#990; }', u'p { border:5px solid green;}', 'body { color:#fff; }']
|
||||
hunks = [h for m, h in self.css_node.hunks()]
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
def test_css_output(self):
|
||||
out = u'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
|
||||
hunks = '\n'.join([h for m, h in self.css_node.hunks()])
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
def test_css_mtimes(self):
|
||||
is_date = re.compile(r'^\d{10}[\.\d]+$')
|
||||
for date in self.css_node.mtimes:
|
||||
self.assertTrue(is_date.match(str(float(date))),
|
||||
"mtimes is returning something that doesn't look like a date: %s" % date)
|
||||
|
||||
def test_css_return_if_off(self):
|
||||
settings.COMPRESS_ENABLED = False
|
||||
self.assertEqual(self.css, self.css_node.output())
|
||||
|
||||
def test_cachekey(self):
|
||||
is_cachekey = re.compile(r'\w{12}')
|
||||
self.assertTrue(is_cachekey.match(self.css_node.cachekey),
|
||||
"cachekey is returning something that doesn't look like r'\w{12}'")
|
||||
|
||||
def test_css_return_if_on(self):
|
||||
output = css_tag('/media/CACHE/css/e41ba2cc6982.css')
|
||||
self.assertEqual(output, self.css_node.output().strip())
|
||||
|
||||
def test_js_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', '<script src="/media/js/one.js" type="text/javascript"></script>'),
|
||||
(SOURCE_HUNK, u'obj.value = "value";', None, '<script type="text/javascript">obj.value = "value";</script>')
|
||||
]
|
||||
split = self.js_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_js_hunks(self):
|
||||
out = ['obj = {};', u'obj.value = "value";']
|
||||
hunks = [h for m, h in self.js_node.hunks()]
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
def test_js_output(self):
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(out, self.js_node.output())
|
||||
|
||||
def test_js_return_if_off(self):
|
||||
try:
|
||||
enabled = settings.COMPRESS_ENABLED
|
||||
precompilers = settings.COMPRESS_PRECOMPILERS
|
||||
settings.COMPRESS_ENABLED = False
|
||||
settings.COMPRESS_PRECOMPILERS = {}
|
||||
self.assertEqual(self.js, self.js_node.output())
|
||||
finally:
|
||||
settings.COMPRESS_ENABLED = enabled
|
||||
settings.COMPRESS_PRECOMPILERS = precompilers
|
||||
|
||||
def test_js_return_if_on(self):
|
||||
output = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(output, self.js_node.output())
|
||||
|
||||
def test_custom_output_dir(self):
|
||||
try:
|
||||
old_output_dir = settings.COMPRESS_OUTPUT_DIR
|
||||
settings.COMPRESS_OUTPUT_DIR = 'custom'
|
||||
output = u'<script type="text/javascript" src="/media/custom/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
settings.COMPRESS_OUTPUT_DIR = ''
|
||||
output = u'<script type="text/javascript" src="/media/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
settings.COMPRESS_OUTPUT_DIR = '/custom/nested/'
|
||||
output = u'<script type="text/javascript" src="/media/custom/nested/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(output, JsCompressor(self.js).output())
|
||||
finally:
|
||||
settings.COMPRESS_OUTPUT_DIR = old_output_dir
|
||||
|
||||
|
||||
|
||||
class CssMediaTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.css = """\
|
||||
<link rel="stylesheet" href="/media/css/one.css" type="text/css" media="screen">
|
||||
<style type="text/css" media="print">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="/media/css/two.css" type="text/css" media="all">
|
||||
<style type="text/css">h1 { border:5px solid green;}</style>"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_css_output(self):
|
||||
links = BeautifulSoup(self.css_node.output()).findAll('link')
|
||||
media = [u'screen', u'print', u'all', None]
|
||||
self.assertEqual(len(links), 4)
|
||||
self.assertEqual(media, [l.get('media', None) for l in links])
|
||||
|
||||
def test_avoid_reordering_css(self):
|
||||
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
|
||||
node = CssCompressor(css)
|
||||
media = [u'screen', u'print', u'all', None, u'print']
|
||||
links = BeautifulSoup(node.output()).findAll('link')
|
||||
self.assertEqual(media, [l.get('media', None) for l in links])
|
||||
|
||||
|
||||
|
||||
|
||||
class VerboseTestCase(CompressorTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VerboseTestCase, self).setUp()
|
||||
settings.COMPRESS_VERBOSE = True
|
||||
|
||||
|
||||
class CacheBackendTestCase(CompressorTestCase):
|
||||
|
||||
def test_correct_backend(self):
|
||||
from compressor.cache import cache
|
||||
self.assertEqual(cache.__class__, locmem.CacheClass)
|
||||
|
||||
|
||||
202
tests/tests/filters.py
Normal file
202
tests/tests/filters.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
import sys
|
||||
from unittest2 import skipIf
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor.cache import get_hashed_mtime
|
||||
from compressor.conf import settings
|
||||
from compressor.css import CssCompressor
|
||||
from compressor.utils import find_command
|
||||
from compressor.filters.base import CompilerFilter
|
||||
|
||||
from .templatetags import render
|
||||
from .base import css_tag, test_dir
|
||||
|
||||
|
||||
class CssTidyTestCase(TestCase):
|
||||
def test_tidy(self):
|
||||
content = """
|
||||
/* Some comment */
|
||||
font,th,td,p{
|
||||
color: black;
|
||||
}
|
||||
"""
|
||||
from compressor.filters.csstidy import CSSTidyFilter
|
||||
self.assertEqual(
|
||||
"font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
|
||||
|
||||
CssTidyTestCase = skipIf(
|
||||
find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
|
||||
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY
|
||||
)(CssTidyTestCase)
|
||||
|
||||
|
||||
class CompassTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.old_debug = settings.DEBUG
|
||||
self.old_compress_css_filters = settings.COMPRESS_CSS_FILTERS
|
||||
self.old_compress_url = settings.COMPRESS_URL
|
||||
self.old_enabled = settings.COMPRESS_ENABLED
|
||||
settings.DEBUG = True
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_CSS_FILTERS = [
|
||||
'compressor.filters.compass.CompassFilter',
|
||||
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
]
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
|
||||
def tearDown(self):
|
||||
settings.DEBUG = self.old_debug
|
||||
settings.COMPRESS_URL = self.old_compress_url
|
||||
settings.COMPRESS_ENABLED = self.old_enabled
|
||||
settings.COMPRESS_CSS_FILTERS = self.old_compress_css_filters
|
||||
|
||||
def test_compass(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/screen.scss" type="text/css">
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/print.scss" type="text/css">
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = {'MEDIA_URL': settings.COMPRESS_URL}
|
||||
out = css_tag("/media/CACHE/css/8ff1cfd8787d.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
CompassTestCase = skipIf(
|
||||
find_command(settings.COMPRESS_COMPASS_BINARY) is None,
|
||||
'Compass binary %r not found' % settings.COMPRESS_COMPASS_BINARY
|
||||
)(CompassTestCase)
|
||||
|
||||
|
||||
class PrecompilerTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.filename = os.path.join(test_dir, 'media/css/one.css')
|
||||
with open(self.filename) as f:
|
||||
self.content = f.read()
|
||||
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
|
||||
|
||||
def test_precompiler_infile_outfile(self):
|
||||
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
|
||||
self.assertEqual(u"body { color:#990; }", compiler.input())
|
||||
|
||||
def test_precompiler_infile_stdout(self):
|
||||
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_outfile(self):
|
||||
command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_stdout(self):
|
||||
command = '%s %s' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=None, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
|
||||
def test_precompiler_stdin_stdout_filename(self):
|
||||
command = '%s %s' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
|
||||
self.assertEqual(u"body { color:#990; }\n", compiler.input())
|
||||
|
||||
|
||||
class CssMinTestCase(TestCase):
|
||||
def test_cssmin_filter(self):
|
||||
from compressor.filters.cssmin import CSSMinFilter
|
||||
content = """p {
|
||||
|
||||
|
||||
background: rgb(51,102,153) url('../../images/image.gif');
|
||||
|
||||
|
||||
}
|
||||
"""
|
||||
output = "p{background:#369 url('../../images/image.gif')}"
|
||||
self.assertEqual(output, CSSMinFilter(content).output())
|
||||
|
||||
|
||||
class CssAbsolutizingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/url/url1.css" type="text/css">
|
||||
<link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_css_absolute_filter(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'http://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_https(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_relative_path(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_hunks(self):
|
||||
hash_dict = {
|
||||
'hash1': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/url1.css')),
|
||||
'hash2': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/2/url2.css')),
|
||||
}
|
||||
out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict,
|
||||
u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict]
|
||||
hunks = [h for m, h in self.css_node.hunks()]
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
|
||||
|
||||
class CssDataUriTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_CSS_FILTERS = [
|
||||
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
'compressor.filters.datauri.CssDataUriFilter',
|
||||
]
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/datauri.css" type="text/css">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def test_data_uris(self):
|
||||
datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/datauri.css'))
|
||||
out = [u'.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }\n.python { background-image: url("/media/img/python.png?%s"); }\n.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }\n' % datauri_hash]
|
||||
hunks = [h for m, h in self.css_node.hunks()]
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
|
||||
|
||||
82
tests/tests/offline.py
Normal file
82
tests/tests/offline.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
|
||||
from django.template import Template, Context
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor.conf import settings
|
||||
from compressor.management.commands.compress import Command as CompressCommand
|
||||
|
||||
from .base import test_dir, css_tag
|
||||
|
||||
class OfflineGenerationTestCase(TestCase):
|
||||
"""Uses templates/test_compressor_offline.html"""
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
self._old_compress = settings.COMPRESS_ENABLED
|
||||
self._old_compress_offline = settings.COMPRESS_OFFLINE
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_OFFLINE = True
|
||||
self.template_file = open(os.path.join(test_dir, "templates/test_compressor_offline.html"))
|
||||
self.template = Template(self.template_file.read().decode(settings.FILE_CHARSET))
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_ENABLED = self._old_compress
|
||||
settings.COMPRESS_OFFLINE = self._old_compress_offline
|
||||
self.template_file.close()
|
||||
|
||||
def test_offline(self):
|
||||
count, result = CompressCommand().compress()
|
||||
self.assertEqual(5, count)
|
||||
self.assertEqual([
|
||||
css_tag('/media/CACHE/css/cd579b7deb7d.css')+'\n',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js"></script>',
|
||||
u'<link rel="stylesheet" href="/media/CACHE/css/67ed6aff7f7b.css" type="text/css" />\n',
|
||||
], result)
|
||||
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
|
||||
# key<->values ourselves?
|
||||
rendered_template = self.template.render(Context({})).replace("\n", "")
|
||||
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
|
||||
|
||||
def test_offline_with_context(self):
|
||||
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
|
||||
settings.COMPRESS_OFFLINE_CONTEXT = {
|
||||
'color': 'blue',
|
||||
}
|
||||
count, result = CompressCommand().compress()
|
||||
self.assertEqual(5, count)
|
||||
self.assertEqual([
|
||||
css_tag('/media/CACHE/css/ee62fbfd116a.css')+'\n',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js"></script>',
|
||||
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js"></script>',
|
||||
u'<link rel="stylesheet" href="/media/CACHE/css/73e015f740c6.css" type="text/css" />\n',
|
||||
], result)
|
||||
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
|
||||
# key<->values ourselves?
|
||||
rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)).replace("\n", "")
|
||||
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
|
||||
settings.COMPRESS_OFFLINE_CONTEXT = self._old_offline_context
|
||||
|
||||
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
|
||||
80
tests/tests/parsers.py
Normal file
80
tests/tests/parsers.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
from unittest2 import skipIf
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
try:
|
||||
import lxml
|
||||
except ImportError:
|
||||
lxml = None
|
||||
|
||||
try:
|
||||
import html5lib
|
||||
except ImportError:
|
||||
html5lib = None
|
||||
|
||||
try:
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
except ImportError:
|
||||
BeautifulSoup = None
|
||||
|
||||
|
||||
from compressor.conf import settings
|
||||
from compressor.base import SOURCE_HUNK, SOURCE_FILE
|
||||
|
||||
from .base import CompressorTestCase
|
||||
|
||||
|
||||
class ParserTestCase(object):
|
||||
|
||||
def setUp(self):
|
||||
self.old_parser = settings.COMPRESS_PARSER
|
||||
settings.COMPRESS_PARSER = self.parser_cls
|
||||
super(ParserTestCase, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_PARSER = self.old_parser
|
||||
|
||||
|
||||
class LxmlParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.LxmlParser'
|
||||
LxmlParserTests = skipIf(lxml is None, 'lxml not found')(LxmlParserTests)
|
||||
|
||||
|
||||
class Html5LibParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.Html5LibParser'
|
||||
|
||||
def test_css_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link href="/media/css/one.css" rel="stylesheet" type="text/css">'),
|
||||
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link href="/media/css/two.css" rel="stylesheet" type="text/css">'),
|
||||
]
|
||||
split = self.css_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
def test_js_split(self):
|
||||
out = [
|
||||
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', u'<script src="/media/js/one.js" type="text/javascript"></script>'),
|
||||
(SOURCE_HUNK, u'obj.value = "value";', None, u'<script type="text/javascript">obj.value = "value";</script>'),
|
||||
]
|
||||
split = self.js_node.split_contents()
|
||||
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
|
||||
self.assertEqual(out, split)
|
||||
|
||||
Html5LibParserTests = skipIf(
|
||||
html5lib is None, 'html5lib not found')(Html5LibParserTests)
|
||||
|
||||
|
||||
class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.BeautifulSoupParser'
|
||||
|
||||
BeautifulSoupParserTests = skipIf(
|
||||
BeautifulSoup is None, 'BeautifulSoup not found')(BeautifulSoupParserTests)
|
||||
|
||||
|
||||
class HtmlParserTests(ParserTestCase, CompressorTestCase):
|
||||
parser_cls = 'compressor.parser.HtmlParser'
|
||||
|
||||
33
tests/tests/storages.py
Normal file
33
tests/tests/storages.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor import base
|
||||
from compressor.conf import settings
|
||||
|
||||
from .base import css_tag
|
||||
from .templatetags import render
|
||||
|
||||
|
||||
class StorageTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self._storage = base.default_storage
|
||||
base.default_storage = get_storage_class(
|
||||
'compressor.storage.GzipCompressorFileStorage')()
|
||||
settings.COMPRESS_ENABLED = True
|
||||
|
||||
def tearDown(self):
|
||||
base.default_storage = self._storage
|
||||
|
||||
def test_css_tag_with_storage(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css">
|
||||
<style type="text/css">p { border:5px solid white;}</style>
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css">
|
||||
{% endcompress %}
|
||||
"""
|
||||
context = {'MEDIA_URL': settings.COMPRESS_URL}
|
||||
out = css_tag("/media/CACHE/css/1d4424458f88.css")
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
99
tests/tests/templatetags.py
Normal file
99
tests/tests/templatetags.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
from django.template import Template, Context, TemplateSyntaxError
|
||||
from django.test import TestCase
|
||||
|
||||
from compressor.conf import settings
|
||||
|
||||
from .base import css_tag
|
||||
|
||||
|
||||
def render(template_string, context_dict=None):
|
||||
"""
|
||||
A shortcut for testing template output.
|
||||
"""
|
||||
if context_dict is None:
|
||||
context_dict = {}
|
||||
c = Context(context_dict)
|
||||
t = Template(template_string)
|
||||
return t.render(c).strip()
|
||||
|
||||
|
||||
class TemplatetagTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.old_enabled = settings.COMPRESS_ENABLED
|
||||
settings.COMPRESS_ENABLED = True
|
||||
self.context = {'MEDIA_URL': settings.COMPRESS_URL}
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_ENABLED = self.old_enabled
|
||||
|
||||
def test_empty_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}{% block js %}
|
||||
{% endblock %}{% endcompress %}"""
|
||||
self.assertEqual(u'', render(template, self.context))
|
||||
|
||||
def test_css_tag(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css">
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css">
|
||||
{% endcompress %}"""
|
||||
out = css_tag("/media/CACHE/css/e41ba2cc6982.css")
|
||||
self.assertEqual(out, render(template, self.context))
|
||||
|
||||
def test_nonascii_css_tag(self):
|
||||
template = u"""{% load compress %}{% compress css %}
|
||||
<link rel="stylesheet" href="{{ MEDIA_URL }}css/nonasc.css" type="text/css">
|
||||
<style type="text/css">p { border:5px solid green;}</style>
|
||||
{% endcompress %}
|
||||
"""
|
||||
out = css_tag("/media/CACHE/css/799f6defe43c.css")
|
||||
self.assertEqual(out, render(template, self.context))
|
||||
|
||||
def test_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">obj.value = "value";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
|
||||
self.assertEqual(out, render(template, self.context))
|
||||
|
||||
def test_nonascii_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/nonasc.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">var test_value = "\u2014";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/e214fe629b28.js"></script>'
|
||||
self.assertEqual(out, render(template, self.context))
|
||||
|
||||
def test_nonascii_latin1_js_tag(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
|
||||
<script type="text/javascript">var test_value = "\u2014";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
out = u'<script type="text/javascript" src="/media/CACHE/js/be9e078b5ca7.js"></script>'
|
||||
self.assertEqual(out, render(template, self.context))
|
||||
|
||||
def test_compress_tag_with_illegal_arguments(self):
|
||||
template = u"""{% load compress %}{% compress pony %}
|
||||
<script type="pony/application">unicorn</script>
|
||||
{% endcompress %}"""
|
||||
self.assertRaises(TemplateSyntaxError, render, template, {})
|
||||
|
||||
def test_debug_toggle(self):
|
||||
template = u"""{% load compress %}{% compress js %}
|
||||
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">obj.value = "value";</script>
|
||||
{% endcompress %}
|
||||
"""
|
||||
class MockDebugRequest(object):
|
||||
GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'}
|
||||
context = dict(self.context, request=MockDebugRequest())
|
||||
out = u"""<script src="/media/js/one.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">obj.value = "value";</script>"""
|
||||
self.assertEqual(out, render(template, context))
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
[testenv]
|
||||
[tox]
|
||||
setupdir = ..
|
||||
distribute = false
|
||||
sitepackages = true
|
||||
downloadcache = {toxinidir}/_download/
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
{envpython} compressor/tests/runtests.py []
|
||||
{envpython} {toxinidir}/runtests.py []
|
||||
coverage html -d {envtmpdir}/coverage
|
||||
|
||||
[testenv:docs]
|
||||
changedir = docs
|
||||
changedir = ../docs
|
||||
deps =
|
||||
Sphinx
|
||||
commands =
|
||||
Reference in New Issue
Block a user