From 74884f33e2f469e190b2019cb412c491c185463f Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 03:48:21 -0500 Subject: [PATCH 1/6] Fix to shlex.split for *_(BINARY|ARGUMENTS) settings on windows shlex.split(), without posix=False, strips "\" characters, which is obviously problematic on windows. --- pipeline/conf.py | 3 ++- tests/tests/test_conf.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pipeline/conf.py b/pipeline/conf.py index 941722c6..37760ffd 100644 --- a/pipeline/conf.py +++ b/pipeline/conf.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import os import collections import shlex @@ -101,7 +102,7 @@ def __getitem__(self, key): value = self.settings[key] if key.endswith(("_BINARY", "_ARGUMENTS")): if isinstance(value, string_types): - return tuple(shlex.split(value)) + return tuple(shlex.split(value, posix=(os.name == 'posix'))) return tuple(value) return value diff --git a/tests/tests/test_conf.py b/tests/tests/test_conf.py index f502faa4..861fe000 100644 --- a/tests/tests/test_conf.py +++ b/tests/tests/test_conf.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import sys +from unittest import skipIf, skipUnless + from django.test import TestCase from pipeline.conf import PipelineSettings @@ -23,10 +26,16 @@ def test_expected_splitting(self): s = PipelineSettings({"FOO_BINARY": "env actualprogram"}) self.assertEqual(s.FOO_BINARY, ('env', 'actualprogram')) + @skipIf(sys.platform.startswith("win"), "requires posix platform") def test_expected_preservation(self): s = PipelineSettings({"FOO_BINARY": r"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): s = PipelineSettings({"FOO_ARGUMENTS": ("explicit", "with", "args")}) self.assertEqual(s.FOO_ARGUMENTS, ('explicit', 'with', 'args')) From fd0be33aead6cb6ff130b0f81d81c35f87500eda Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 03:56:09 -0500 Subject: [PATCH 2/6] Set stdout and stderr to blocking after running an external command Some nodejs executables set stdout and/or stderr to non-blocking, which can trigger an IOError when the collectstatic command prints more than 1024 bytes at a time to stdout or stderr --- pipeline/compilers/__init__.py | 3 ++- pipeline/compressors/__init__.py | 3 ++- pipeline/utils.py | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pipeline/compilers/__init__.py b/pipeline/compilers/__init__.py index 1c345fb4..b3edd4f9 100644 --- a/pipeline/compilers/__init__.py +++ b/pipeline/compilers/__init__.py @@ -12,7 +12,7 @@ from pipeline.conf import settings from pipeline.exceptions import CompilerError -from pipeline.utils import to_class +from pipeline.utils import to_class, set_std_streams_blocking class Compiler(object): @@ -120,6 +120,7 @@ def execute_command(self, command, cwd=None, stdout_captured=None): stdout=stdout, stderr=subprocess.PIPE) _, stderr = compiling.communicate() + set_std_streams_blocking() if compiling.returncode != 0: stdout_captured = None # Don't save erroneous result. diff --git a/pipeline/compressors/__init__.py b/pipeline/compressors/__init__.py index 4926ea76..6043a242 100644 --- a/pipeline/compressors/__init__.py +++ b/pipeline/compressors/__init__.py @@ -14,7 +14,7 @@ from pipeline.conf import settings 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_REPLACER = r"""url\(__EMBED__(.+?)(\?\d+)?\)""" @@ -248,6 +248,7 @@ def execute_command(self, command, content): if content: content = smart_bytes(content) stdout, stderr = pipe.communicate(content) + set_std_streams_blocking() if stderr.strip() and pipe.returncode != 0: raise CompressorError(stderr) elif self.verbose: diff --git a/pipeline/utils.py b/pipeline/utils.py index dc5380b3..729667f6 100644 --- a/pipeline/utils.py +++ b/pipeline/utils.py @@ -1,8 +1,16 @@ from __future__ import unicode_literals +try: + import fcntl +except ImportError: + # windows + fcntl = None + import importlib import mimetypes import posixpath +import os +import sys try: from urllib.parse import quote @@ -54,3 +62,19 @@ def relpath(path, start=posixpath.curdir): if not rel_list: return posixpath.curdir 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) From bb864e302fa641dc771318905e543269718a600d Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 03:58:12 -0500 Subject: [PATCH 3/6] Set a default value of os.environ['NUMBER_OF_PROCESSORS'] on windows If this environment variable is not set, multiprocessing throws a NotImplementedError --- tests/tests/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/tests/__init__.py b/tests/tests/__init__.py index 68f94d69..1c36c0c5 100644 --- a/tests/tests/__init__.py +++ b/tests/tests/__init__.py @@ -1,4 +1,12 @@ # -*- 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_compressor import * from .test_template import * From e74ab5cea2916ab734bfeba2e223da47244652b6 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 14:40:44 -0500 Subject: [PATCH 4/6] Allow tox to be run without any arguments for all tests --- tox.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 79fca98f..d13fd4d1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist = - {py27,pypy,py34}-django{18,19} - py35-django19 - docs + {py27,pypy,py34}-django{18,19},py35-django19,docs [testenv] basepython = From 6d3e470df9094f38cce3b07eea8e215e5268e6f5 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 14:48:08 -0500 Subject: [PATCH 5/6] Add unit tests for compiler implementations --- .gitignore | 4 ++ MANIFEST.in | 3 + tests/assets/compilers/coffee/expected.js | 12 ++++ tests/assets/compilers/coffee/input.coffee | 2 + tests/assets/compilers/es6/expected.js | 27 +++++++++ tests/assets/compilers/es6/input.es6 | 19 ++++++ tests/assets/compilers/less/expected.css | 3 + tests/assets/compilers/less/input.less | 5 ++ tests/assets/compilers/livescript/expected.js | 6 ++ tests/assets/compilers/livescript/input.ls | 2 + tests/assets/compilers/scss/expected.css | 5 ++ tests/assets/compilers/scss/input.scss | 10 ++++ tests/assets/compilers/stylus/expected.css | 3 + tests/assets/compilers/stylus/input.styl | 2 + tests/package.json | 22 +++++++ tests/scripts/npm_install.py | 42 +++++++++++++ tests/settings.py | 23 +++++++ tests/tests/test_compiler.py | 60 ++++++++++++++++++- tox.ini | 1 + 19 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 tests/assets/compilers/coffee/expected.js create mode 100644 tests/assets/compilers/coffee/input.coffee create mode 100644 tests/assets/compilers/es6/expected.js create mode 100644 tests/assets/compilers/es6/input.es6 create mode 100644 tests/assets/compilers/less/expected.css create mode 100644 tests/assets/compilers/less/input.less create mode 100644 tests/assets/compilers/livescript/expected.js create mode 100644 tests/assets/compilers/livescript/input.ls create mode 100644 tests/assets/compilers/scss/expected.css create mode 100644 tests/assets/compilers/scss/input.scss create mode 100644 tests/assets/compilers/stylus/expected.css create mode 100644 tests/assets/compilers/stylus/input.styl create mode 100644 tests/package.json create mode 100755 tests/scripts/npm_install.py diff --git a/.gitignore b/.gitignore index 02e621ef..2aa42b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ docs/_build/ coverage/ tests/static/ tests/assets/js/dummy.js +tests/node_modules/ .tox/ .DS_Store .idea @@ -21,3 +22,6 @@ tests/assets/js/dummy.js .pydevproject .ropeproject __pycache__ +npm-debug.log +tests/npm-cache +django-pipeline-*/ diff --git a/MANIFEST.in b/MANIFEST.in index b5128c78..c91427a3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,8 @@ recursive-include pipeline/jinja2 *.html *.jinja include AUTHORS LICENSE README.rst HISTORY.rst recursive-include tests * 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 recursive-include docs *.rst diff --git a/tests/assets/compilers/coffee/expected.js b/tests/assets/compilers/coffee/expected.js new file mode 100644 index 00000000..468864e8 --- /dev/null +++ b/tests/assets/compilers/coffee/expected.js @@ -0,0 +1,12 @@ +(function() { + var cube, square; + + square = function(x) { + return x * x; + }; + + cube = function(x) { + return square(x) * x; + }; + +}).call(this); diff --git a/tests/assets/compilers/coffee/input.coffee b/tests/assets/compilers/coffee/input.coffee new file mode 100644 index 00000000..60cc7ca4 --- /dev/null +++ b/tests/assets/compilers/coffee/input.coffee @@ -0,0 +1,2 @@ +square = (x) -> x * x +cube = (x) -> square(x) * x diff --git a/tests/assets/compilers/es6/expected.js b/tests/assets/compilers/es6/expected.js new file mode 100644 index 00000000..726d9807 --- /dev/null +++ b/tests/assets/compilers/es6/expected.js @@ -0,0 +1,27 @@ +"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); + }); + } +}; diff --git a/tests/assets/compilers/es6/input.es6 b/tests/assets/compilers/es6/input.es6 new file mode 100644 index 00000000..bcd7420d --- /dev/null +++ b/tests/assets/compilers/es6/input.es6 @@ -0,0 +1,19 @@ +// 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)); + } +}; diff --git a/tests/assets/compilers/less/expected.css b/tests/assets/compilers/less/expected.css new file mode 100644 index 00000000..08ef0d9f --- /dev/null +++ b/tests/assets/compilers/less/expected.css @@ -0,0 +1,3 @@ +.a { + width: 1px; +} diff --git a/tests/assets/compilers/less/input.less b/tests/assets/compilers/less/input.less new file mode 100644 index 00000000..f271f5b6 --- /dev/null +++ b/tests/assets/compilers/less/input.less @@ -0,0 +1,5 @@ +@a: 1; + +.a { + width: (@a + 0px); +} diff --git a/tests/assets/compilers/livescript/expected.js b/tests/assets/compilers/livescript/expected.js new file mode 100644 index 00000000..cbac6734 --- /dev/null +++ b/tests/assets/compilers/livescript/expected.js @@ -0,0 +1,6 @@ +(function(){ + var times; + times = function(x, y){ + return x * y; + }; +}).call(this); diff --git a/tests/assets/compilers/livescript/input.ls b/tests/assets/compilers/livescript/input.ls new file mode 100644 index 00000000..2d18794d --- /dev/null +++ b/tests/assets/compilers/livescript/input.ls @@ -0,0 +1,2 @@ +times = (x, y) -> + x * y diff --git a/tests/assets/compilers/scss/expected.css b/tests/assets/compilers/scss/expected.css new file mode 100644 index 00000000..450aae02 --- /dev/null +++ b/tests/assets/compilers/scss/expected.css @@ -0,0 +1,5 @@ +.a .b { + display: none; } + +.c .d { + display: block; } diff --git a/tests/assets/compilers/scss/input.scss b/tests/assets/compilers/scss/input.scss new file mode 100644 index 00000000..10cb5a29 --- /dev/null +++ b/tests/assets/compilers/scss/input.scss @@ -0,0 +1,10 @@ +.a { + .b { + display: none; + } +} +.c { + .d { + display: block; + } +} diff --git a/tests/assets/compilers/stylus/expected.css b/tests/assets/compilers/stylus/expected.css new file mode 100644 index 00000000..8ca9cd9e --- /dev/null +++ b/tests/assets/compilers/stylus/expected.css @@ -0,0 +1,3 @@ +.a { + color: #000; +} diff --git a/tests/assets/compilers/stylus/input.styl b/tests/assets/compilers/stylus/input.styl new file mode 100644 index 00000000..4de78cc4 --- /dev/null +++ b/tests/assets/compilers/stylus/input.styl @@ -0,0 +1,2 @@ +.a + color: black \ No newline at end of file diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 00000000..b4a276b0 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,22 @@ +{ + "name": "django-pipeline-tests", + "private": true, + "version": "1.0.0", + "description": "Pipeline is an asset packaging library for Django.", + "author": "Timothée Peignier ", + "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" + } +} diff --git a/tests/scripts/npm_install.py b/tests/scripts/npm_install.py new file mode 100755 index 00000000..fd5861f3 --- /dev/null +++ b/tests/scripts/npm_install.py @@ -0,0 +1,42 @@ +#!/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() diff --git a/tests/settings.py b/tests/settings.py index 5b0ac1d1..30faef7a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,4 +1,5 @@ import os +import distutils.spawn def local_path(path): @@ -118,6 +119,28 @@ def local_path(path): } } +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') +HAS_NODE = os.path.exists(NODE_BIN_PATH) and NODE_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'], + }) + TEMPLATE_DIRS = ( local_path('templates'), ) diff --git a/tests/tests/test_compiler.py b/tests/tests/test_compiler.py index d908e2a9..c800c996 100644 --- a/tests/tests/test_compiler.py +++ b/tests/tests/test_compiler.py @@ -1,13 +1,18 @@ from __future__ import unicode_literals 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.client import RequestFactory +from django.utils.encoding import smart_bytes from pipeline.collector import default_collector from pipeline.compilers import Compiler, CompilerBase, SubProcessCompiler from pipeline.exceptions import CompilerError +from pipeline.utils import to_class from tests.utils import _, pipeline_settings @@ -169,3 +174,56 @@ def test_compile(self): def tearDown(self): 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') diff --git a/tox.ini b/tox.ini index d13fd4d1..01ce4fd3 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ setenv = DJANGO_SETTINGS_MODULE = tests.settings PYTHONPATH = {toxinidir} commands = + {toxinidir}/tests/scripts/npm_install.py {envbindir}/django-admin.py test {posargs:tests} [testenv:docs] From c17fd826183337cb3829020ca007ec11260385d1 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 28 Jan 2016 14:48:28 -0500 Subject: [PATCH 6/6] Add unit tests for compressor implementations --- tests/assets/compressors/closure.js | 1 + tests/assets/compressors/cssmin.css | 1 + tests/assets/compressors/csstidy.css | 1 + tests/assets/compressors/jsmin.js | 1 + tests/assets/compressors/slimit.js | 1 + tests/assets/compressors/uglifyjs.js | 1 + tests/assets/compressors/yuglify.css | 1 + tests/assets/compressors/yuglify.js | 1 + tests/assets/compressors/yui.css | 1 + tests/assets/compressors/yui.js | 1 + tests/package.json | 7 ++- tests/settings.py | 23 +++++++ tests/tests/test_compressor.py | 93 +++++++++++++++++++++++++++- tox.ini | 3 + 14 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tests/assets/compressors/closure.js create mode 100644 tests/assets/compressors/cssmin.css create mode 100644 tests/assets/compressors/csstidy.css create mode 100644 tests/assets/compressors/jsmin.js create mode 100644 tests/assets/compressors/slimit.js create mode 100644 tests/assets/compressors/uglifyjs.js create mode 100644 tests/assets/compressors/yuglify.css create mode 100644 tests/assets/compressors/yuglify.js create mode 100644 tests/assets/compressors/yui.css create mode 100644 tests/assets/compressors/yui.js diff --git a/tests/assets/compressors/closure.js b/tests/assets/compressors/closure.js new file mode 100644 index 00000000..6dbca2a2 --- /dev/null +++ b/tests/assets/compressors/closure.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this); diff --git a/tests/assets/compressors/cssmin.css b/tests/assets/compressors/cssmin.css new file mode 100644 index 00000000..824c6c3c --- /dev/null +++ b/tests/assets/compressors/cssmin.css @@ -0,0 +1 @@ +.concat{display:none}.concatenate{display:block} \ No newline at end of file diff --git a/tests/assets/compressors/csstidy.css b/tests/assets/compressors/csstidy.css new file mode 100644 index 00000000..acaa8cfc --- /dev/null +++ b/tests/assets/compressors/csstidy.css @@ -0,0 +1 @@ +.concat{display:none;}.concatenate{display:block;} \ No newline at end of file diff --git a/tests/assets/compressors/jsmin.js b/tests/assets/compressors/jsmin.js new file mode 100644 index 00000000..83d6f479 --- /dev/null +++ b/tests/assets/compressors/jsmin.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments);}}());(function(){window.cat=function(){console.log("hello world");}}());}).call(this); \ No newline at end of file diff --git a/tests/assets/compressors/slimit.js b/tests/assets/compressors/slimit.js new file mode 100644 index 00000000..87baf7ca --- /dev/null +++ b/tests/assets/compressors/slimit.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments);};}());(function(){window.cat=function(){console.log("hello world");};}());}).call(this); \ No newline at end of file diff --git a/tests/assets/compressors/uglifyjs.js b/tests/assets/compressors/uglifyjs.js new file mode 100644 index 00000000..6dbca2a2 --- /dev/null +++ b/tests/assets/compressors/uglifyjs.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments)}})();(function(){window.cat=function(){console.log("hello world")}})()}).call(this); diff --git a/tests/assets/compressors/yuglify.css b/tests/assets/compressors/yuglify.css new file mode 100644 index 00000000..3ea0434c --- /dev/null +++ b/tests/assets/compressors/yuglify.css @@ -0,0 +1 @@ +.concat{display:none}.concatenate{display:block} diff --git a/tests/assets/compressors/yuglify.js b/tests/assets/compressors/yuglify.js new file mode 100644 index 00000000..77ca92f1 --- /dev/null +++ b/tests/assets/compressors/yuglify.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments)}})(),function(){window.cat=function(){console.log("hello world")}}()}).call(this); diff --git a/tests/assets/compressors/yui.css b/tests/assets/compressors/yui.css new file mode 100644 index 00000000..824c6c3c --- /dev/null +++ b/tests/assets/compressors/yui.css @@ -0,0 +1 @@ +.concat{display:none}.concatenate{display:block} \ No newline at end of file diff --git a/tests/assets/compressors/yui.js b/tests/assets/compressors/yui.js new file mode 100644 index 00000000..5a001c4d --- /dev/null +++ b/tests/assets/compressors/yui.js @@ -0,0 +1 @@ +(function(){(function(){window.concat=function(){console.log(arguments)}}());(function(){window.cat=function(){console.log("hello world")}}())}).call(this); \ No newline at end of file diff --git a/tests/package.json b/tests/package.json index b4a276b0..1d0f782e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -17,6 +17,11 @@ "less": "^2.5.3", "livescript": "^1.4.0", "node-sass": "^3.4.2", - "stylus": "^0.53.0" + "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" } } diff --git a/tests/settings.py b/tests/settings.py index 30faef7a..3b87cdf5 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,3 +1,4 @@ +import glob import os import distutils.spawn @@ -59,6 +60,8 @@ def local_path(path): PIPELINE = { 'PIPELINE_ENABLED': True, + 'JS_COMPRESSOR': None, + 'CSS_COMPRESSOR': None, 'STYLESHEETS': { 'screen': { 'source_filenames': ( @@ -122,7 +125,11 @@ def local_path(path): 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): @@ -139,8 +146,24 @@ def node_exe_path(command): '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 = ( local_path('templates'), ) diff --git a/tests/tests/test_compressor.py b/tests/tests/test_compressor.py index d5fee9cb..4291b515 100644 --- a/tests/tests/test_compressor.py +++ b/tests/tests/test_compressor.py @@ -11,18 +11,24 @@ except ImportError: 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.client import RequestFactory +from django.utils.encoding import smart_bytes -from pipeline.compressors import Compressor, TEMPLATE_FUNC, \ - SubProcessCompressor +from pipeline.compressors import ( + Compressor, TEMPLATE_FUNC, SubProcessCompressor) from pipeline.compressors.yuglify import YuglifyCompressor from pipeline.collector import default_collector from tests.utils import _, pipeline_settings +@pipeline_settings( + CSS_COMPRESSOR='pipeline.compressors.yuglify.YuglifyCompressor', + JS_COMPRESSOR='pipeline.compressors.yuglify.YuglifyCompressor') class CompressorTest(TestCase): def setUp(self): self.maxDiff = None @@ -186,3 +192,84 @@ def test_compressor_subprocess_unicode(self): def tearDown(self): 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') + + diff --git a/tox.ini b/tox.ini index 01ce4fd3..f62636cb 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,9 @@ deps = django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 jinja2 + jsmin==2.2.0 + ply==3.4 + slimit==0.8.1 setenv = DJANGO_SETTINGS_MODULE = tests.settings PYTHONPATH = {toxinidir}