Commit 6346a202 by David Trowbridge

Merge pull request #531 from theatlantic/pr/compiler-compressor-unit-tests

Add unit tests for the bundled compiler and compressor classes
parents 0ed51f71 c17fd826
...@@ -13,6 +13,7 @@ docs/_build/ ...@@ -13,6 +13,7 @@ docs/_build/
coverage/ coverage/
tests/static/ tests/static/
tests/assets/js/dummy.js tests/assets/js/dummy.js
tests/node_modules/
.tox/ .tox/
.DS_Store .DS_Store
.idea .idea
...@@ -21,3 +22,6 @@ tests/assets/js/dummy.js ...@@ -21,3 +22,6 @@ tests/assets/js/dummy.js
.pydevproject .pydevproject
.ropeproject .ropeproject
__pycache__ __pycache__
npm-debug.log
tests/npm-cache
django-pipeline-*/
...@@ -3,5 +3,8 @@ recursive-include pipeline/jinja2 *.html *.jinja ...@@ -3,5 +3,8 @@ recursive-include pipeline/jinja2 *.html *.jinja
include AUTHORS LICENSE README.rst HISTORY.rst include AUTHORS LICENSE README.rst HISTORY.rst
recursive-include tests * recursive-include tests *
recursive-exclude tests *.pyc *.pyo recursive-exclude tests *.pyc *.pyo
recursive-exclude tests/node_modules *
recursive-exclude tests/npm-cache *
recursive-exclude tests/npm *
include docs/Makefile docs/make.bat docs/conf.py include docs/Makefile docs/make.bat docs/conf.py
recursive-include docs *.rst recursive-include docs *.rst
...@@ -12,7 +12,7 @@ from django.utils.six import string_types ...@@ -12,7 +12,7 @@ from django.utils.six import string_types
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.exceptions import CompilerError from pipeline.exceptions import CompilerError
from pipeline.utils import to_class from pipeline.utils import to_class, set_std_streams_blocking
class Compiler(object): class Compiler(object):
...@@ -120,6 +120,7 @@ class SubProcessCompiler(CompilerBase): ...@@ -120,6 +120,7 @@ class SubProcessCompiler(CompilerBase):
stdout=stdout, stdout=stdout,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
_, stderr = compiling.communicate() _, stderr = compiling.communicate()
set_std_streams_blocking()
if compiling.returncode != 0: if compiling.returncode != 0:
stdout_captured = None # Don't save erroneous result. stdout_captured = None # Don't save erroneous result.
......
...@@ -14,7 +14,7 @@ from django.utils.six import string_types ...@@ -14,7 +14,7 @@ from django.utils.six import string_types
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.exceptions import CompressorError from pipeline.exceptions import CompressorError
from pipeline.utils import to_class, relpath from pipeline.utils import to_class, relpath, set_std_streams_blocking
URL_DETECTOR = r"""url\((['"]){0,1}\s*(.*?)["']{0,1}\)""" URL_DETECTOR = r"""url\((['"]){0,1}\s*(.*?)["']{0,1}\)"""
URL_REPLACER = r"""url\(__EMBED__(.+?)(\?\d+)?\)""" URL_REPLACER = r"""url\(__EMBED__(.+?)(\?\d+)?\)"""
...@@ -248,6 +248,7 @@ class SubProcessCompressor(CompressorBase): ...@@ -248,6 +248,7 @@ class SubProcessCompressor(CompressorBase):
if content: if content:
content = smart_bytes(content) content = smart_bytes(content)
stdout, stderr = pipe.communicate(content) stdout, stderr = pipe.communicate(content)
set_std_streams_blocking()
if stderr.strip() and pipe.returncode != 0: if stderr.strip() and pipe.returncode != 0:
raise CompressorError(stderr) raise CompressorError(stderr)
elif self.verbose: elif self.verbose:
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import collections import collections
import shlex import shlex
...@@ -101,7 +102,7 @@ class PipelineSettings(collections.MutableMapping): ...@@ -101,7 +102,7 @@ class PipelineSettings(collections.MutableMapping):
value = self.settings[key] value = self.settings[key]
if key.endswith(("_BINARY", "_ARGUMENTS")): if key.endswith(("_BINARY", "_ARGUMENTS")):
if isinstance(value, string_types): if isinstance(value, string_types):
return tuple(shlex.split(value)) return tuple(shlex.split(value, posix=(os.name == 'posix')))
return tuple(value) return tuple(value)
return value return value
......
from __future__ import unicode_literals from __future__ import unicode_literals
try:
import fcntl
except ImportError:
# windows
fcntl = None
import importlib import importlib
import mimetypes import mimetypes
import posixpath import posixpath
import os
import sys
try: try:
from urllib.parse import quote from urllib.parse import quote
...@@ -54,3 +62,19 @@ def relpath(path, start=posixpath.curdir): ...@@ -54,3 +62,19 @@ def relpath(path, start=posixpath.curdir):
if not rel_list: if not rel_list:
return posixpath.curdir return posixpath.curdir
return posixpath.join(*rel_list) return posixpath.join(*rel_list)
def set_std_streams_blocking():
"""
Set stdout and stderr to be blocking.
This is called after Popen.communicate() to revert stdout and stderr back
to be blocking (the default) in the event that the process to which they
were passed manipulated one or both file descriptors to be non-blocking.
"""
if not fcntl:
return
for f in (sys.__stdout__, sys.__stderr__):
fileno = f.fileno()
flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
(function() {
var cube, square;
square = function(x) {
return x * x;
};
cube = function(x) {
return square(x) * x;
};
}).call(this);
square = (x) -> x * x
cube = (x) -> square(x) * x
"use strict";
// Expression bodies
var odds = evens.map(function (v) {
return v + 1;
});
var nums = evens.map(function (v, i) {
return v + i;
});
// Statement bodies
nums.forEach(function (v) {
if (v % 5 === 0) fives.push(v);
});
// Lexical this
var bob = {
_name: "Bob",
_friends: [],
printFriends: function printFriends() {
var _this = this;
this._friends.forEach(function (f) {
return console.log(_this._name + " knows " + f);
});
}
};
// Expression bodies
var odds = evens.map(v => v + 1);
var nums = evens.map((v, i) => v + i);
// Statement bodies
nums.forEach(v => {
if (v % 5 === 0)
fives.push(v);
});
// Lexical this
var bob = {
_name: "Bob",
_friends: [],
printFriends() {
this._friends.forEach(f =>
console.log(this._name + " knows " + f));
}
};
@a: 1;
.a {
width: (@a + 0px);
}
(function(){
var times;
times = function(x, y){
return x * y;
};
}).call(this);
.a .b {
display: none; }
.c .d {
display: block; }
.a {
.b {
display: none;
}
}
.c {
.d {
display: block;
}
}
.a
color: black
\ No newline at end of file
(function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this);
.concat{display:none}.concatenate{display:block}
\ No newline at end of file
.concat{display:none;}.concatenate{display:block;}
\ No newline at end of file
(function(){(function(){window.concat=function(){console.log(arguments);}}());(function(){window.cat=function(){console.log("hello world");}}());}).call(this);
\ No newline at end of file
(function(){(function(){window.concat=function(){console.log(arguments);};}());(function(){window.cat=function(){console.log("hello world");};}());}).call(this);
\ No newline at end of file
(function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this);
.concat{display:none}.concatenate{display:block}
(function(){(function(){window.concat=function(){console.log(arguments)}})(),function(){window.cat=function(){console.log("hello world")}}()}).call(this);
.concat{display:none}.concatenate{display:block}
\ No newline at end of file
(function(){(function(){window.concat=function(){console.log(arguments)}}());(function(){window.cat=function(){console.log("hello world")}}())}).call(this);
\ No newline at end of file
{
"name": "django-pipeline-tests",
"private": true,
"version": "1.0.0",
"description": "Pipeline is an asset packaging library for Django.",
"author": "Timothée Peignier <timothee.peignier@tryphon.org>",
"license": "MIT",
"readmeFilename": "../README.rst",
"repository": {
"type": "git",
"url": "git://github.com/jazzband/django-pipeline.git"
},
"dependencies": {
"babel-cli": "^6.4.5",
"babel-preset-es2015": "^6.3.13",
"coffee-script": "^1.10.0",
"less": "^2.5.3",
"livescript": "^1.4.0",
"node-sass": "^3.4.2",
"stylus": "^0.53.0",
"cssmin": "^0.4.3",
"google-closure-compiler": "^20151216.2.0",
"uglify-js": "^2.6.1",
"yuglify": "^0.1.4",
"yuicompressor": "^2.4.8"
}
}
#!/usr/bin/env python
"""
A cross-platform compatible `npm install` call, checking whether npm is
in fact installed on the system first (and on windows, checking that the
npm version is at least 3.0 because of a bug in 2.x with MAX_PATH)
"""
import distutils.spawn
import os
from pkg_resources import parse_version
import re
import subprocess
import sys
def main():
tests_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
if os.name == 'nt':
try:
npm_paths = subprocess.check_output(['where', 'npm.cmd'])
except subprocess.CalledProcessError:
return
else:
npm_bin = re.split(r'\r?\n', npm_paths)[0]
else:
npm_bin = distutils.spawn.find_executable('npm')
if not npm_bin:
return
if os.name == 'nt':
os.environ.setdefault('APPDATA', '.')
npm_version = subprocess.check_output([npm_bin, '--version']).strip()
# Skip on windows if npm version is less than 3 because of
# MAX_PATH issues in version 2
if parse_version(npm_version) < parse_version('3.0'):
return
pipe = subprocess.Popen([npm_bin, 'install'],
cwd=tests_dir, stdout=sys.stdout, stderr=sys.stderr)
pipe.communicate()
sys.exit(pipe.returncode)
if __name__ == '__main__':
main()
import glob
import os import os
import distutils.spawn
def local_path(path): def local_path(path):
...@@ -58,6 +60,8 @@ SECRET_KEY = "django-pipeline" ...@@ -58,6 +60,8 @@ SECRET_KEY = "django-pipeline"
PIPELINE = { PIPELINE = {
'PIPELINE_ENABLED': True, 'PIPELINE_ENABLED': True,
'JS_COMPRESSOR': None,
'CSS_COMPRESSOR': None,
'STYLESHEETS': { 'STYLESHEETS': {
'screen': { 'screen': {
'source_filenames': ( 'source_filenames': (
...@@ -118,6 +122,48 @@ PIPELINE = { ...@@ -118,6 +122,48 @@ PIPELINE = {
} }
} }
NODE_MODULES_PATH = local_path('node_modules')
NODE_BIN_PATH = os.path.join(NODE_MODULES_PATH, '.bin')
NODE_EXE_PATH = distutils.spawn.find_executable('node')
JAVA_EXE_PATH = distutils.spawn.find_executable('java')
CSSTIDY_EXE_PATH = distutils.spawn.find_executable('csstidy')
HAS_NODE = os.path.exists(NODE_BIN_PATH) and NODE_EXE_PATH
HAS_JAVA = bool(JAVA_EXE_PATH)
HAS_CSSTIDY = bool(CSSTIDY_EXE_PATH)
if HAS_NODE:
def node_exe_path(command):
exe_ext = '.cmd' if os.name == 'nt' else ''
return os.path.join(NODE_BIN_PATH, "%s%s" % (command, exe_ext))
PIPELINE.update({
'SASS_BINARY': node_exe_path('node-sass'),
'COFFEE_SCRIPT_BINARY': node_exe_path('coffee'),
'COFFEE_SCRIPT_ARGUMENTS': ['--no-header'],
'LESS_BINARY': node_exe_path('lessc'),
'BABEL_BINARY': node_exe_path('babel'),
'BABEL_ARGUMENTS': ['--presets', 'es2015'],
'STYLUS_BINARY': node_exe_path('stylus'),
'LIVE_SCRIPT_BINARY': node_exe_path('lsc'),
'LIVE_SCRIPT_ARGUMENTS': ['--no-header'],
'YUGLIFY_BINARY': node_exe_path('yuglify'),
'UGLIFYJS_BINARY': node_exe_path('uglifyjs'),
'CSSMIN_BINARY': node_exe_path('cssmin'),
})
if HAS_NODE and HAS_JAVA:
PIPELINE.update({
'CLOSURE_BINARY': [
JAVA_EXE_PATH, '-jar',
os.path.join(NODE_MODULES_PATH, 'google-closure-compiler', 'compiler.jar')],
'YUI_BINARY': [
JAVA_EXE_PATH, '-jar',
glob.glob(os.path.join(NODE_MODULES_PATH, 'yuicompressor', 'build', '*.jar'))[0]]
})
if HAS_CSSTIDY:
PIPELINE.update({'CSSTIDY_BINARY': CSSTIDY_EXE_PATH})
TEMPLATE_DIRS = ( TEMPLATE_DIRS = (
local_path('templates'), local_path('templates'),
) )
......
# -*- coding: utf-8 flake8: noqa -*- # -*- coding: utf-8 flake8: noqa -*-
import os
import sys
if sys.platform.startswith('win'):
os.environ.setdefault('NUMBER_OF_PROCESSORS', '1')
from .test_compiler import * from .test_compiler import *
from .test_compressor import * from .test_compressor import *
from .test_template import * from .test_template import *
......
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import sys
from unittest import skipIf from unittest import skipIf, skipUnless
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.encoding import smart_bytes
from pipeline.collector import default_collector from pipeline.collector import default_collector
from pipeline.compilers import Compiler, CompilerBase, SubProcessCompiler from pipeline.compilers import Compiler, CompilerBase, SubProcessCompiler
from pipeline.exceptions import CompilerError from pipeline.exceptions import CompilerError
from pipeline.utils import to_class
from tests.utils import _, pipeline_settings from tests.utils import _, pipeline_settings
...@@ -169,3 +174,56 @@ class FailingCompilerTest(TestCase): ...@@ -169,3 +174,56 @@ class FailingCompilerTest(TestCase):
def tearDown(self): def tearDown(self):
default_collector.clear() default_collector.clear()
@skipUnless(settings.HAS_NODE, "requires node")
class CompilerImplementation(TestCase):
def setUp(self):
self.compiler = Compiler()
default_collector.collect(RequestFactory().get('/'))
def tearDown(self):
default_collector.clear()
def _test_compiler(self, compiler_cls_str, infile, expected):
compiler_cls = to_class(compiler_cls_str)
compiler = compiler_cls(verbose=False, storage=staticfiles_storage)
infile_path = staticfiles_storage.path(infile)
outfile_path = compiler.output_path(infile_path, compiler.output_extension)
compiler.compile_file(_(infile_path), _(outfile_path), force=True)
with open(outfile_path) as f:
result = f.read()
with staticfiles_storage.open(expected) as f:
expected = f.read()
self.assertEqual(smart_bytes(result), expected)
def test_sass(self):
self._test_compiler('pipeline.compilers.sass.SASSCompiler',
'pipeline/compilers/scss/input.scss',
'pipeline/compilers/scss/expected.css')
def test_coffeescript(self):
self._test_compiler('pipeline.compilers.coffee.CoffeeScriptCompiler',
'pipeline/compilers/coffee/input.coffee',
'pipeline/compilers/coffee/expected.js')
def test_less(self):
self._test_compiler('pipeline.compilers.less.LessCompiler',
'pipeline/compilers/less/input.less',
'pipeline/compilers/less/expected.css')
def test_es6(self):
self._test_compiler('pipeline.compilers.es6.ES6Compiler',
'pipeline/compilers/es6/input.es6',
'pipeline/compilers/es6/expected.js')
def test_stylus(self):
self._test_compiler('pipeline.compilers.stylus.StylusCompiler',
'pipeline/compilers/stylus/input.styl',
'pipeline/compilers/stylus/expected.css')
def test_livescript(self):
self._test_compiler('pipeline.compilers.livescript.LiveScriptCompiler',
'pipeline/compilers/livescript/input.ls',
'pipeline/compilers/livescript/expected.js')
...@@ -11,18 +11,24 @@ try: ...@@ -11,18 +11,24 @@ try:
except ImportError: except ImportError:
from unittest.mock import patch # noqa from unittest.mock import patch # noqa
from unittest import skipIf from unittest import skipIf, skipUnless
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.encoding import smart_bytes
from pipeline.compressors import Compressor, TEMPLATE_FUNC, \ from pipeline.compressors import (
SubProcessCompressor Compressor, TEMPLATE_FUNC, SubProcessCompressor)
from pipeline.compressors.yuglify import YuglifyCompressor from pipeline.compressors.yuglify import YuglifyCompressor
from pipeline.collector import default_collector from pipeline.collector import default_collector
from tests.utils import _, pipeline_settings from tests.utils import _, pipeline_settings
@pipeline_settings(
CSS_COMPRESSOR='pipeline.compressors.yuglify.YuglifyCompressor',
JS_COMPRESSOR='pipeline.compressors.yuglify.YuglifyCompressor')
class CompressorTest(TestCase): class CompressorTest(TestCase):
def setUp(self): def setUp(self):
self.maxDiff = None self.maxDiff = None
...@@ -186,3 +192,84 @@ class CompressorTest(TestCase): ...@@ -186,3 +192,84 @@ class CompressorTest(TestCase):
def tearDown(self): def tearDown(self):
default_collector.clear() default_collector.clear()
class CompressorImplementationTest(TestCase):
maxDiff = None
def setUp(self):
self.compressor = Compressor()
default_collector.collect(RequestFactory().get('/'))
def tearDown(self):
default_collector.clear()
def _test_compressor(self, compressor_cls, compress_type, expected_file):
override_settings = {
("%s_COMPRESSOR" % compress_type.upper()): compressor_cls,
}
with pipeline_settings(**override_settings):
if compress_type == 'js':
result = self.compressor.compress_js(
[_('pipeline/js/first.js'), _('pipeline/js/second.js')])
else:
result = self.compressor.compress_css(
[_('pipeline/css/first.css'), _('pipeline/css/second.css')],
os.path.join('pipeline', 'css', os.path.basename(expected_file)))
with self.compressor.storage.open(expected_file) as f:
expected = f.read()
self.assertEqual(smart_bytes(result), expected)
def test_jsmin(self):
self._test_compressor('pipeline.compressors.jsmin.JSMinCompressor',
'js', 'pipeline/compressors/jsmin.js')
def test_slimit(self):
self._test_compressor('pipeline.compressors.slimit.SlimItCompressor',
'js', 'pipeline/compressors/slimit.js')
@skipUnless(settings.HAS_NODE, "requires node")
def test_uglifyjs(self):
self._test_compressor('pipeline.compressors.uglifyjs.UglifyJSCompressor',
'js', 'pipeline/compressors/uglifyjs.js')
@skipUnless(settings.HAS_NODE, "requires node")
def test_yuglify_js(self):
self._test_compressor('pipeline.compressors.yuglify.YuglifyCompressor',
'js', 'pipeline/compressors/yuglify.js')
@skipUnless(settings.HAS_NODE, "requires node")
def test_yuglify_css(self):
self._test_compressor('pipeline.compressors.yuglify.YuglifyCompressor',
'css', 'pipeline/compressors/yuglify.css')
@skipUnless(settings.HAS_NODE, "requires node")
def test_cssmin(self):
self._test_compressor('pipeline.compressors.cssmin.CSSMinCompressor',
'css', 'pipeline/compressors/cssmin.css')
@skipUnless(settings.HAS_NODE, "requires node")
@skipUnless(settings.HAS_JAVA, "requires java")
def test_closure(self):
self._test_compressor('pipeline.compressors.closure.ClosureCompressor',
'js', 'pipeline/compressors/closure.js')
@skipUnless(settings.HAS_NODE, "requires node")
@skipUnless(settings.HAS_JAVA, "requires java")
def test_yui_js(self):
self._test_compressor('pipeline.compressors.yui.YUICompressor',
'js', 'pipeline/compressors/yui.js')
@skipUnless(settings.HAS_NODE, "requires node")
@skipUnless(settings.HAS_JAVA, "requires java")
def test_yui_css(self):
self._test_compressor('pipeline.compressors.yui.YUICompressor',
'css', 'pipeline/compressors/yui.css')
@skipUnless(settings.HAS_CSSTIDY, "requires csstidy")
def test_csstidy(self):
self._test_compressor('pipeline.compressors.csstidy.CSSTidyCompressor',
'css', 'pipeline/compressors/csstidy.css')
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
from unittest import skipIf, skipUnless
from django.test import TestCase from django.test import TestCase
from pipeline.conf import PipelineSettings from pipeline.conf import PipelineSettings
...@@ -23,10 +26,16 @@ class TestSettings(TestCase): ...@@ -23,10 +26,16 @@ class TestSettings(TestCase):
s = PipelineSettings({"FOO_BINARY": "env actualprogram"}) s = PipelineSettings({"FOO_BINARY": "env actualprogram"})
self.assertEqual(s.FOO_BINARY, ('env', 'actualprogram')) self.assertEqual(s.FOO_BINARY, ('env', 'actualprogram'))
@skipIf(sys.platform.startswith("win"), "requires posix platform")
def test_expected_preservation(self): def test_expected_preservation(self):
s = PipelineSettings({"FOO_BINARY": r"actual\ program"}) s = PipelineSettings({"FOO_BINARY": r"actual\ program"})
self.assertEqual(s.FOO_BINARY, ('actual program',)) self.assertEqual(s.FOO_BINARY, ('actual program',))
@skipUnless(sys.platform.startswith("win"), "requires windows")
def test_win_path_preservation(self):
s = PipelineSettings({"FOO_BINARY": "C:\\Test\\ActualProgram.exe argument"})
self.assertEqual(s.FOO_BINARY, ('C:\\Test\\ActualProgram.exe', 'argument'))
def test_tuples_are_normal(self): def test_tuples_are_normal(self):
s = PipelineSettings({"FOO_ARGUMENTS": ("explicit", "with", "args")}) s = PipelineSettings({"FOO_ARGUMENTS": ("explicit", "with", "args")})
self.assertEqual(s.FOO_ARGUMENTS, ('explicit', 'with', 'args')) self.assertEqual(s.FOO_ARGUMENTS, ('explicit', 'with', 'args'))
[tox] [tox]
envlist = envlist =
{py27,pypy,py34}-django{18,19} {py27,pypy,py34}-django{18,19},py35-django19,docs
py35-django19
docs
[testenv] [testenv]
basepython = basepython =
...@@ -16,10 +14,14 @@ deps = ...@@ -16,10 +14,14 @@ deps =
django18: Django>=1.8,<1.9 django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10 django19: Django>=1.9,<1.10
jinja2 jinja2
jsmin==2.2.0
ply==3.4
slimit==0.8.1
setenv = setenv =
DJANGO_SETTINGS_MODULE = tests.settings DJANGO_SETTINGS_MODULE = tests.settings
PYTHONPATH = {toxinidir} PYTHONPATH = {toxinidir}
commands = commands =
{toxinidir}/tests/scripts/npm_install.py
{envbindir}/django-admin.py test {posargs:tests} {envbindir}/django-admin.py test {posargs:tests}
[testenv:docs] [testenv:docs]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment