From 4f7323f7fcbbf5277d9703e61845b088365f84a6 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 8 Sep 2012 14:03:21 -0700 Subject: [PATCH] Add test/test-build.js. Former-commit-id: b0c28b814dec71095a927469cbbda766fd9fc701 --- build.js | 64 ++++-- build/minify.js | 4 +- test/test-build.js | 551 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 600 insertions(+), 19 deletions(-) create mode 100644 test/test-build.js diff --git a/build.js b/build.js index 91614770a..f8ce5ad36 100755 --- a/build.js +++ b/build.js @@ -178,7 +178,7 @@ 'some': ['identity'], 'sortBy': [], 'sortedIndex': ['bind'], - 'tap': [], + 'tap': ['mixin'], 'template': ['escape'], 'throttle': [], 'times': [], @@ -187,6 +187,7 @@ 'union': ['indexOf'], 'uniq': ['identity', 'indexOf'], 'uniqueId': [], + 'value': ['mixin'], 'values': ['isArguments'], 'where': ['forIn'], 'without': ['indexOf'], @@ -217,11 +218,10 @@ 'useStrict' ]; - /** Collections of method names */ - var excludeMethods = [], - includeMethods = [], - allMethods = _.keys(dependencyMap); + /** List of all Lo-Dash methods */ + var allMethods = _.keys(dependencyMap); + /** List of methods used by Underscore */ var underscoreMethods = _.without.apply(_, [allMethods].concat([ 'countBy', 'forIn', @@ -239,6 +239,23 @@ /*--------------------------------------------------------------------------*/ + /** + * Removes unnecessary comments and whitespace. + * + * @private + * @param {String} source The source to process. + * @returns {String} Returns the modified source. + */ + function cleanupSource(source) { + return source + // remove lines with just whitespace and semicolons + .replace(/^ *;\n/gm, '') + // consolidate consecutive horizontal rule comment separators + .replace(/(?:\s*\/\*-+\*\/\s*){2,}/g, function(separators) { + return separators.match(/^\s*/)[0] + separators.slice(separators.lastIndexOf('/*')); + }); + } + /** * Logs the help message to the console. * @@ -264,7 +281,9 @@ '', ' Options:', '', + ' -c , --stdout Write output to standard output', ' -h, --help Display help information', + ' -o, --output Write output to a given filename/path', ' -s, --silent Skip status updates normally logged to the console', ' -V, --version Output current version of Lo-Dash', '' @@ -650,6 +669,10 @@ // the debug version of `source` var debugSource; + // collections of method names to exclude or include + var excludeMethods = [], + includeMethods = []; + // flag used to specify a Backbone build var isBackbone = options.indexOf('backbone') > -1; @@ -680,7 +703,10 @@ // load customized Lo-Dash module var lodash = (function() { - var sandbox = {}; + var context = _.extend(vm.createContext(), { + 'clearTimeout': clearTimeout, + 'setTimeout': setTimeout + }); if (isStrict) { source = setUseStrictOption(source, true); @@ -735,8 +761,8 @@ .replace(/(?: *\/\/.*\n)*\s*' *(?:<% *)?if *\(!hasDontEnumBug *(?:&&|\))[\s\S]+?<% *} *(?:%>|').+/g, '') .replace(/!hasDontEnumBug *\|\|/g, ''); } - vm.runInNewContext(source, sandbox); - return sandbox._; + vm.runInContext(source, context); + return context._; }()); // used to specify whether filtering is for exclusion or inclusion @@ -762,7 +788,7 @@ // used to report invalid arguments var invalidArgs = _.reject(options, function(value) { - if (/^(?:category|exclude|include)=(?:.*)$/.test(value)) { + if (/^(?:category|exclude|include)=.*$/.test(value)) { return true; } return [ @@ -772,7 +798,10 @@ 'mobile', 'strict', 'underscore', + '-c', '--stdout', '-h', '--help', + '-o', '--output', + '-s', '--silent', '-V', '--version' ].indexOf(value) > -1; }); @@ -857,7 +886,7 @@ return false; } // resolve method names belonging to each category - var categoryMethods = categories.reduce(function(result, category) { + var categoryMethods = categories[1].split(/, */).reduce(function(result, category) { return result.concat(allMethods.filter(function(funcName) { return RegExp('@category ' + category + '\\b', 'i').test(matchFunction(source, funcName)); })); @@ -1141,6 +1170,9 @@ debugSource = source; // remove associated functions, variables, and code snippets that the minifier may miss + if (isRemoved(source, 'clone')) { + source = source.replace(/(?:\n +\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)?\n *var cloneableClasses *=[\s\S]+?true;\n/g, ''); + } if (isRemoved(source, 'isArray')) { source = removeVar(source, 'nativeIsArray'); } @@ -1178,6 +1210,9 @@ if (isRemoved(source, 'createIterator', 'bind', 'isArray', 'keys')) { source = removeVar(source, 'reNative'); } + if (isRemoved(source, 'createIterator', 'isEmpty', 'isEqual')) { + source = source.replace(/(?:\n +\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)?\n *var arrayLikeClasses *=[\s\S]+?true;\n/g, ''); + } if (isRemoved(source, 'createIterator', 'isEqual')) { source = source.replace(/(?:\n +\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)?\n *var hasDontEnumBug;|.+?hasDontEnumBug *=.+/g, ''); } @@ -1192,13 +1227,8 @@ source = source.replace(/ *\(function\(\) *{[\s\S]+?}\(1\)\);/, ''); } - // consolidate consecutive horizontal rule comment separators - source = source.replace(/(?:\s*\/\*-+\*\/\s*){2,}/g, function(separators) { - return separators.match(/^\s*/)[0] + separators.slice(separators.lastIndexOf('/*')); - }); - - // cleanup code - source = source.replace(/^ *;\n/gm, ''); + debugSource = cleanupSource(debugSource); + source = cleanupSource(source); // begin the minification process if (filterType || isBackbone || isLegacy || isMobile || isStrict || isUnderscore) { diff --git a/build/minify.js b/build/minify.js index 6b9343b74..de4d3b429 100755 --- a/build/minify.js +++ b/build/minify.js @@ -361,10 +361,10 @@ var filePath = options[options.length - 1], dirPath = path.dirname(filePath), + workingName = path.basename(filePath, '.js') + '.min', outputPath = path.join(dirPath, workingName + '.js'), isSilent = options.indexOf('-s') > -1 || options.indexOf('--silent') > -1, - source = fs.readFileSync(filePath, 'utf8'), - workingName = path.basename(filePath, '.js') + '.min'; + source = fs.readFileSync(filePath, 'utf8'); minify(source, { 'silent': isSilent, diff --git a/test/test-build.js b/test/test-build.js new file mode 100644 index 000000000..764036e13 --- /dev/null +++ b/test/test-build.js @@ -0,0 +1,551 @@ +#!/usr/bin/env node +;(function() { + 'use strict'; + + /** Load modules */ + var fs = require('fs'), + path = require('path'), + vm = require('vm'); + + /** The unit testing framework */ + var QUnit = global.QUnit = require('../vendor/qunit/qunit/qunit.js'); + require('../vendor/qunit-clib/qunit-clib.js'); + + /** The `lodash` function to test */ + var _ = require('../lodash.js'); + + /** The `build` module */ + var build = require('../build.js'); + + /** Used to access the built Lo-Dash object */ + var context = vm.createContext({ + 'clearTimeout': clearTimeout, + 'setTimeout': setTimeout + }); + + /** Used to associate aliases with their real names */ + var aliasToRealMap = { + 'all': 'every', + 'any': 'some', + 'collect': 'map', + 'detect': 'find', + 'drop': 'rest', + 'each': 'forEach', + 'foldl': 'reduce', + 'foldr': 'reduceRight', + 'head': 'first', + 'include': 'contains', + 'inject': 'reduce', + 'methods': 'functions', + 'select': 'filter', + 'tail': 'rest', + 'take': 'first', + 'unique': 'uniq' + }; + + /** Used to associate real names with their aliases */ + var realToAliasMap = { + 'contains': ['include'], + 'every': ['all'], + 'filter': ['select'], + 'find': ['detect'], + 'first': ['head', 'take'], + 'forEach': ['each'], + 'functions': ['methods'], + 'map': ['collect'], + 'reduce': ['foldl', 'inject'], + 'reduceRight': ['foldr'], + 'rest': ['drop', 'tail'], + 'some': ['any'], + 'uniq': ['unique'] + }; + + /** List of all Lo-Dash methods */ + var allMethods = _.functions(_).filter(function(methodName) { + return !/^_/.test(methodName); + }); + + /** List of "Arrays" category methods */ + var arraysMethods = [ + 'compact', + 'difference', + 'drop', + 'first', + 'flatten', + 'head', + 'indexOf', + 'initial', + 'intersection', + 'last', + 'lastIndexOf', + 'max', + 'min', + 'object', + 'range', + 'rest', + 'shuffle', + 'sortedIndex', + 'tail', + 'take', + 'union', + 'uniq', + 'unique', + 'without', + 'zip' + ]; + + /** List of "Chaining" category methods */ + var chainingMethods = [ + 'chain', + 'mixin', + 'tap', + 'value' + ]; + + /** List of "Collections" category methods */ + var collectionsMethods = [ + 'all', + 'any', + 'collect', + 'contains', + 'countBy', + 'detect', + 'each', + 'every', + 'filter', + 'find', + 'foldl', + 'foldr', + 'forEach', + 'groupBy', + 'include', + 'inject', + 'invoke', + 'map', + 'pluck', + 'reduce', + 'reduceRight', + 'reject', + 'select', + 'size', + 'some', + 'sortBy', + 'toArray', + 'where' + ]; + + /** List of "Functions" category methods */ + var functionsMethods = [ + 'after', + 'bind', + 'bindAll', + 'compose', + 'debounce', + 'defer', + 'delay', + 'memoize', + 'once', + 'partial', + 'throttle', + 'wrap' + ]; + + /** List of "Objects" category methods */ + var objectsMethods = [ + 'clone', + 'defaults', + 'extend', + 'forIn', + 'forOwn', + 'functions', + 'has', + 'invert', + 'isArguments', + 'isArray', + 'isBoolean', + 'isDate', + 'isElement', + 'isEmpty', + 'isEqual', + 'isFinite', + 'isFunction', + 'isNaN', + 'isNull', + 'isNumber', + 'isObject', + 'isRegExp', + 'isString', + 'isUndefined', + 'keys', + 'methods', + 'merge', + 'omit', + 'pairs', + 'pick', + 'values' + ]; + + /** List of "Utilities" category methods */ + var utilityMethods = [ + 'escape', + 'identity', + 'noConflict', + 'random', + 'result', + 'template', + 'times', + 'unescape', + 'uniqueId' + ]; + + /** List of Backbone's Lo-Dash dependencies */ + var backboneDependencies = [ + 'bind', + 'bindAll', + 'clone', + 'contains', + 'escape', + 'every', + 'extend', + 'filter', + 'find', + 'first', + 'forEach', + 'groupBy', + 'has', + 'indexOf', + 'initial', + 'invoke', + 'isArray', + 'isEmpty', + 'isEqual', + 'isFunction', + 'isObject', + 'isRegExp', + 'keys', + 'last', + 'lastIndexOf', + 'map', + 'max', + 'min', + 'mixin', + 'reduce', + 'reduceRight', + 'reject', + 'rest', + 'result', + 'shuffle', + 'size', + 'some', + 'sortBy', + 'sortedIndex', + 'toArray', + 'uniqueId', + 'without' + ]; + + /** List of methods used by Underscore */ + var underscoreMethods = _.without.apply(_, [allMethods].concat([ + 'countBy', + 'forIn', + 'forOwn', + 'invert', + 'merge', + 'object', + 'omit', + 'pairs', + 'partial', + 'random', + 'unescape', + 'where' + ])); + + /*--------------------------------------------------------------------------*/ + + /** + * Expands a list of method names to include real and alias names. + * + * @private + * @param {Array} methodNames The array of method names to expand. + * @returns {Array} Returns a new array of expanded method names. + */ + function expandMethodNames(methodNames) { + return methodNames.reduce(function(result, methodName) { + var realName = getRealName(methodName); + result.push.apply(result, [realName].concat(getAliases(realName))); + return result; + }, []); + } + + /** + * Gets the aliases associated with a given function name. + * + * @private + * @param {String} funcName The name of the function to get aliases for. + * @returns {Array} Returns an array of aliases. + */ + function getAliases(funcName) { + return realToAliasMap[funcName] || []; + } + + /** + * Gets the real name, not alias, of a given function name. + * + * @private + * @param {String} funcName The name of the function to resolve. + * @returns {String} Returns the real name. + */ + function getRealName(funcName) { + return aliasToRealMap[funcName] || funcName; + } + + /** + * Tests if a given method on the `lodash` object can be called successfully. + * + * @private + * @param {Object} lodash The built Lo-Dash object. + * @param {String} methodName The name of the Lo-Dash method to test. + * @param {String} message The unit test message. + */ + function testMethod(lodash, methodName, message) { + var pass = true, + array = [['a', 1], ['b', 2], ['c', 3]], + object = { 'a': 1, 'b': 2, 'c': 3 }, + noop = function() {}, + string = 'abc', + func = lodash[methodName]; + + try { + if (arraysMethods.indexOf(methodName) > -1) { + if (/(?:indexOf|sortedIndex|without)$/i.test(methodName)) { + func(array, string); + } else if (/^(?:difference|intersection|union|uniq|zip)/.test(methodName)) { + func(array, array); + } else if (methodName == 'range') { + func(2, 4); + } else { + func(array); + } + } + else if (chainingMethods.indexOf(methodName) > -1) { + if (methodName == 'chain') { + lodash.chain(array); + lodash(array).chain(); + } + else if (methodName == 'mixin') { + lodash.mixin({}); + } + else { + lodash(array)[methodName](noop); + } + } + else if (collectionsMethods.indexOf(methodName) > -1) { + if (/^(?:count|group|sort)By$/.test(methodName)) { + func(array, noop); + func(array, string); + func(object, noop); + func(object, string); + } + else if (/^(?:size|toArray)$/.test(methodName)) { + func(array); + func(object); + } + else if (methodName == 'invoke') { + func(array, 'slice'); + func(object, 'toFixed'); + } + else if (methodName == 'where') { + func(array, object); + func(object, object); + } + else { + func(array, noop, object); + func(object, noop, object); + } + } + else if (functionsMethods.indexOf(methodName) > -1) { + if (methodName == 'after') { + func(1, noop); + } else if (/^(?:bind|partial)$/.test(methodName)) { + func(noop, object, array, string); + } else if (/^(?:compose|memoize|wrap)$/.test(methodName)) { + func(noop, noop); + } else if (/^(?:debounce|throttle)$/.test(methodName)) { + func(noop, 100); + } else if (methodName == 'bindAll') { + func({ 'noop': noop }); + } else { + func(noop); + } + } + else if (objectsMethods.indexOf(methodName) > -1) { + if (methodName == 'clone') { + func(object); + func(object, true); + } + else if (/^(?:defaults|extend|merge)$/.test(methodName)) { + func({}, object); + } else if (/^(?:forIn|forOwn)$/.test(methodName)) { + func(object, noop); + } else if (/^(?:omit|pick)$/.test(methodName)) { + func(object, 'b'); + } else if (methodName == 'has') { + func(object, string); + } else { + func(object); + } + } + else if (utilityMethods.indexOf(methodName) > -1) { + if (methodName == 'result') { + func(object, 'b'); + } else if (methodName == 'times') { + func(2, noop, object); + } else { + func(string, object); + } + } + } + catch(e) { + console.log(e); + pass = false; + } + equal(pass, true, methodName + ': ' + message); + } + + /*--------------------------------------------------------------------------*/ + + QUnit.module('build'); + + (function() { + var commands = [ + 'backbone', + 'csp', + 'legacy', + 'mobile', + 'strict', + 'underscore', + 'category=arrays', + 'category=chaining', + 'category=collections', + 'category=functions', + 'category=objects', + 'category=utilities', + 'exclude=union,uniq,zip', + 'include=each,filter,map', + 'category=collections,functions', + 'underscore backbone', + 'backbone legacy category=utilities exclude=first,last', + 'underscore mobile strict category=functions include=pick,uniq', + ] + .concat( + allMethods.map(function(methodName) { + return 'include=' + methodName; + }) + ); + + commands.forEach(function(command) { + var start = _.after(2, QUnit.start); + + asyncTest('`lodash ' + command +'`', function() { + build(['--silent'].concat(command.split(' ')), function(filepath, source) { + try { + delete context._; + vm.runInContext(source, context); + } catch(e) { } + + var basename = path.basename(filepath, '.js'), + lodash = context._ || {}, + methodNames = []; + + if (/underscore/.test(command)) { + methodNames = underscoreMethods; + } + if (/backbone/.test(command)) { + methodNames = backboneDependencies; + } + if (/include/.test(command)) { + methodNames = methodNames.concat(command.match(/include=(\S*)/)[1].split(/, */)); + } + if (/category/.test(command)) { + methodNames = command.match(/category=(\S*)/)[1].split(/, */).reduce(function(result, category) { + switch (category) { + case 'arrays': + return result.concat(arraysMethods); + case 'chaining': + return result.concat(chainingMethods); + case 'collections': + return result.concat(collectionsMethods); + case 'functions': + return result.concat(functionsMethods); + case 'objects': + return result.concat(objectsMethods); + case 'utilities': + return result.concat(utilityMethods); + } + return result; + }, methodNames); + } + if (!methodNames.length) { + methodNames = allMethods; + } + + if (/exclude/.test(command)) { + methodNames = _.without.apply(_, [methodNames].concat( + expandMethodNames(command.match(/exclude=(\S*)/)[1].split(/, */)) + )); + } else { + methodNames = expandMethodNames(methodNames); + } + + methodNames = _.unique(methodNames); + + methodNames.forEach(function(methodName) { + testMethod(lodash, methodName, basename); + }); + + start(); + }); + }); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('strict modifier'); + + (function() { + var object = {}; + + Object.defineProperties(object, { + 'a': { 'value': _.identify }, + 'b': { 'value': null } + }); + + ['non-strict', 'strict'].forEach(function(strictMode, index) { + asyncTest(strictMode + ' should ' + (index ? 'error': 'silently fail') + ' attempting to overwrite read-only properties', function() { + var commands = ['--silent', 'include=bindAll,defaults,extend']; + if (index) { + commands.push('strict'); + } + + build(commands, function(filepath, source) { + vm.runInContext(source, context); + + var basename = path.basename(filepath, '.js'), + lodash = context._, + pass = !index; + + try { + lodash.bindAll(object); + lodash.extend(object, { 'a': 1 }); + lodash.defaults(object, { 'b': 2 }); + } catch(e) { + pass = !!index; + } + equal(pass, true, basename); + start(); + }); + }); + }); + }()); +}());