diff --git a/.travis.yml b/.travis.yml index 59030498..06b10939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,16 +3,16 @@ node_js: - "0.12" - "0.10" - "0.8" - - "iojs-v1.4" + - "iojs" env: - TEST="all" - TEST="node" matrix: exclude: - - node_js: "iojs-v1.4" - env: TEST="node" + - node_js: "iojs" + env: TEST="all" - node_js: "0.12" - env: TEST="node" + env: TEST="all" - node_js: "0.10" env: TEST="node" - node_js: "0.8" diff --git a/Gruntfile.js b/Gruntfile.js index 4e8a7654..c20bd595 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -24,9 +24,9 @@ module.exports = function(grunt) { var commandList = [], fs = require('fs'), rhinoFolder = 'test/rhino/'; - fs.readdirSync(__dirname + '/' + rhinoFolder + '/lib').forEach( function(rhinoJar) { + fs.readdirSync(__dirname + '/' + rhinoFolder + 'lib').forEach( function(rhinoJar) { if(rhinoJar.indexOf('.jar') >= 0) { - commandList.push('java -jar ' + rhinoFolder + '/lib/' + rhinoJar + ' -f ' + rhinoFolder + '/rhinoTest.js'); + commandList.push('java -jar ' + rhinoFolder + 'lib/' + rhinoJar + ' -f ' + rhinoFolder + 'rhinoTest.js'); } }); return commandList.join(' && '); @@ -137,7 +137,10 @@ module.exports = function(grunt) { src: 'tmp/dust-full.min.js', options: { keepRunner: false, - specs: ['test/jasmine-test/spec/*.js'] + display: 'short', + helpers: ['test/jasmine-test/spec/coreTests.js'], + specs: ['test/jasmine-test/spec/testHelpers.js', 'test/jasmine-test/spec/renderTestSpec.js'], + vendor: 'node_modules/ayepromise/ayepromise.js' } }, /*tests unminified code, mostly used for debugging by `grunt dev` task*/ @@ -145,7 +148,9 @@ module.exports = function(grunt) { src: 'tmp/dust-full.js', options: { keepRunner: false, - specs : '<%=jasmine.testProd.options.specs%>' + helpers: '<%=jasmine.testProd.options.helpers%>', + specs : '<%=jasmine.testProd.options.specs%>', + vendor: '<%=jasmine.testProd.options.vendor%>' } }, /*runs unit tests with jasmine and collects test coverage info via istanbul template @@ -157,7 +162,10 @@ module.exports = function(grunt) { src: 'tmp/dust-full.js', options: { keepRunner: false, + display: 'none', + helpers: '<%=jasmine.testProd.options.helpers%>', specs : '<%=jasmine.testProd.options.specs%>', + vendor: '<%=jasmine.testProd.options.vendor%>', template: require('grunt-template-jasmine-istanbul'), templateOptions: { coverage: 'tmp/coverage/coverage.json', diff --git a/lib/dust.js b/lib/dust.js index 8bbf2f6f..6a7bd66e 100644 --- a/lib/dust.js +++ b/lib/dust.js @@ -261,7 +261,7 @@ }; function Context(stack, global, blocks, templateName) { - this.stack = stack; + this.stack = stack; this.global = global; this.blocks = blocks; this.templateName = templateName; @@ -271,6 +271,18 @@ return new Context(new Stack(), global); }; + /** + * Factory function that creates a closure scope around a Thenable-callback. + * Returns a function that can be passed to a Thenable that will resume a + * Context lookup once the Thenable resolves with new data, adding that new + * data to the lookup stack. + */ + function getWithResolvedData(ctx, cur, down) { + return function(data) { + return ctx.push(data)._get(cur, down); + }; + } + Context.wrap = function(context, name) { if (context instanceof Context) { return context; @@ -315,6 +327,7 @@ var ctx = this.stack || {}, i = 1, value, first, len, ctxThis, fn; + first = down[0]; len = down.length; @@ -351,6 +364,10 @@ } while (ctx && i < len) { + if (dust.isThenable(ctx)) { + // Bail early by returning a Thenable for the remainder of the search tree + return ctx.then(getWithResolvedData(this, cur, down.slice(i))); + } ctxThis = ctx; ctx = ctx[down[i]]; i++; @@ -578,7 +595,7 @@ } Chunk.prototype.write = function(data) { - var taps = this.taps; + var taps = this.taps; if (taps) { data = taps.go(data); @@ -667,7 +684,7 @@ } } - if (params) { + if (!dust.isEmptyObject(params)) { context = context.push(params); } @@ -786,11 +803,16 @@ }; Chunk.prototype.helper = function(name, context, bodies, params) { - var chunk = this; + var chunk = this, + ret; // handle invalid helpers, similar to invalid filters if(dust.helpers[name]) { try { - return dust.helpers[name](chunk, context, bodies, params); + ret = dust.helpers[name](chunk, context, bodies, params); + if (dust.isThenable(ret)) { + return this.await(ret, context, bodies); + } + return ret; } catch(err) { dust.log('Error in helper `' + name + '`: ' + err.message, ERROR); return chunk.setError(err); diff --git a/package.json b/package.json index 1b4ab958..fcbfc651 100644 --- a/package.json +++ b/package.json @@ -44,20 +44,21 @@ "cli": "~0.6.5" }, "devDependencies": { + "ayepromise": "~1.1.1", "grunt": "~0.4.2", "grunt-bump": "0.0.11", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.3.0", "grunt-contrib-connect": "~0.5.0", "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-jasmine": "~0.5.2", + "grunt-contrib-jasmine": "~0.8.2", "grunt-contrib-jshint": "~0.7.2", "grunt-contrib-uglify": "~0.2.7", "grunt-contrib-watch": "~0.5.3", "grunt-gh-pages": "~0.9.0", "grunt-jasmine-nodejs": "~1.0.2", "grunt-shell": "~0.6.1", - "grunt-template-jasmine-istanbul": "~0.2.5", + "grunt-template-jasmine-istanbul": "~0.3.3", "pegjs": "0.8.0" }, "license": "MIT", diff --git a/test/core.js b/test/core.js index dd660b89..7d00d8b7 100644 --- a/test/core.js +++ b/test/core.js @@ -152,7 +152,11 @@ function testRender(unit, source, context, expected, options, baseContext, error } }); } catch(err) { - unit.contains(error, err.message || err); + if(error) { + unit.contains(error, err.message || err); + } else { + throw err; + } } unit.pass(); }; diff --git a/test/jasmine-test/spec/cli/cliSpec.js b/test/jasmine-test/spec/cli/cliSpec.js index 993b15e0..45f2cfdc 100644 --- a/test/jasmine-test/spec/cli/cliSpec.js +++ b/test/jasmine-test/spec/cli/cliSpec.js @@ -154,7 +154,7 @@ describe('--version', function() { function dustc(args, cb) { var loc = path.join(BIN_DIR, 'dustc'); - exec(loc + ' ' + args, { cwd: FIXTURE_DIR }, cb); + exec('node ' + loc + ' ' + args, { cwd: FIXTURE_DIR }, cb); } function fixture(file) { diff --git a/test/jasmine-test/spec/coreTests.js b/test/jasmine-test/spec/coreTests.js index bb09e4ad..16bac7e7 100755 --- a/test/jasmine-test/spec/coreTests.js +++ b/test/jasmine-test/spec/coreTests.js @@ -1,25 +1,20 @@ +if (typeof require !== 'undefined') { + ayepromise = require('ayepromise'); +} /** - * A naive fake-Promise that simply waits for callbacks - * to be bound by calling `.then` and then invokes - * one of the callbacks asynchronously. + * A naive Promise constructor that simply resolves or rejects its Promise based on what's passed * @param err {*} Invokes the `error` callback with this value * @param data {*} Invokes the `success` callback with this value, if `err` is not set * @return {Object} a fake Promise with a `then` method */ -function FakePromise(err, data) { - function then(success, failure) { - setTimeout(function() { - if(err) { - failure(err); - } else { - success(data); - } - }, 0); +function FalsePromise(err, data) { + var defer = ayepromise.defer(); + if (err) { + defer.reject(new Error(err)); + } else { + defer.resolve(data); } - - return { - "then": then - }; + return defer.promise; } var coreTests = [ @@ -775,35 +770,99 @@ var coreTests = [ { name: "thenable reference", source: "Eventually {magic}!", - context: { "magic": new FakePromise(null, "magic") }, + context: { "magic": new FalsePromise(null, "magic") }, expected: "Eventually magic!", message: "should reserve an async chunk for a thenable reference" }, + { + name: "thenable deep reference", + source: "Eventually {magic.ally.delicious}!", + context: { "magic": new FalsePromise(null, {"ally": {"delicious": "Lucky Charms"} }) }, + expected: "Eventually Lucky Charms!", + message: "should deep-inspect a thenable reference" + }, + { + name: "thenable deep reference that doesn't exist", + source: "Eventually {magic.ally.disappeared}!", + context: { "magic": new FalsePromise(null, {"ally": {"delicious": "Lucky Charms"} }) }, + expected: "Eventually !", + message: "should deep-inspect a thenable reference but move on if it isn't there" + }, + { + name: "thenable deep reference... this is just getting silly", + source: "Eventually {magic.ally.delicious}!", + context: { "magic": new FalsePromise(null, {"ally": {"delicious": new FalsePromise(null, "Lucky Charms")} }) }, + expected: "Eventually Lucky Charms!", + message: "should deep-inspect a thenable reference recursively" + }, + { + name: "thenable reference that fails", + source: "Eventually {magic.ally.delicious}!", + context: { "magic": new FalsePromise("cereal gone") }, + expected: "Eventually !", + message: "should inspect a thenable reference but move on if it fails" + }, + { + name: "thenable deep reference that fails", + source: "Eventually {magic.ally.delicious}!", + context: { "magic": new FalsePromise(null, {"ally": {"delicious": new FalsePromise("cereal gone")} }) }, + expected: "Eventually !", + message: "should deep-inspect a thenable reference but move on if it fails" + }, { name: "thenable section", source: "{#promise}Eventually {magic}!{/promise}", - context: { "promise": new FakePromise(null, {"magic": "magic"}) }, + context: { "promise": new FalsePromise(null, {"magic": "magic"}) }, expected: "Eventually magic!", message: "should reserve an async section for a thenable" }, { name: "thenable section from function", source: "{#promise}Eventually {magic}!{/promise}", - context: { "promise": function() { return new FakePromise(null, {"magic": "magic"}); } }, + context: { "promise": function() { return new FalsePromise(null, {"magic": "magic"}); } }, expected: "Eventually magic!", message: "should reserve an async section for a thenable returned from a function" }, + { + name: "thenable deep section", + source: "Eventually my {#magic.ally}{delicious}{/magic.ally} will come", + context: { "magic": new FalsePromise(null, {"ally": {"delicious": new FalsePromise(null, "Lucky Charms")} }) }, + expected: "Eventually my Lucky Charms will come", + message: "should reserve an async section for a deep-reference thenable" + }, + { + name: "thenable deep section, traverse outside", + source: "Eventually my {#magic.ally}{prince} {delicious} {charms}{/magic.ally} will come", + base: { charms: new FalsePromise(null, "Charms") }, + context: { "prince": "Prince", "magic": new FalsePromise(null, {"ally": {"delicious": new FalsePromise(null, "Lucky")} }) }, + expected: "Eventually my Prince Lucky Charms will come", + message: "should reserve an async section for a deep-reference thenable and not blow the stack" + }, + { + name: "thenable resolved by global helper", + source: '{@promise resolve="helper"}I am a big {.}!{/promise}', + context: {}, + expected: "I am a big helper!", + message: "Dust helpers that return thenables are resolved in context" + }, + { + name: "thenable rejected by global helper", + source: '{@promise reject="error"}I am a big helper!{:error}I am a big {.}!{/promise}', + context: {}, + expected: "I am a big error!", + message: "Dust helpers that return thenables are rejected in context" + }, { name: "thenable error", source: "{promise}", - context: { "promise": new FakePromise("promise error") }, + context: { "promise": new FalsePromise("promise error") }, log: "Unhandled promise rejection in `thenable error`", message: "rejected thenable reference logs" }, { name: "thenable error with error block", source: "{#promise}No magic{:error}{message}{/promise}", - context: { "promise": new FakePromise(new Error("promise error")) }, + context: { "promise": new FalsePromise("promise error") }, expected: "promise error", message: "rejected thenable renders error block" } @@ -1285,8 +1344,8 @@ var coreTests = [ '{>"{parentTemplate}"/} | additional parent output'].join("\n"), context: { "loadTemplate": function(chunk, context, bodies, params) { - var source = dust.testHelpers.tap(params.source, chunk, context), - name = dust.testHelpers.tap(params.name, chunk, context); + var source = context.resolve(params.source), + name = context.resolve(params.name); dust.loadSource(dust.compile(source, name)); return chunk.write(''); }, diff --git a/test/jasmine-test/spec/renderTestSpec.js b/test/jasmine-test/spec/renderTestSpec.js index 0ae73e12..e60de331 100644 --- a/test/jasmine-test/spec/renderTestSpec.js +++ b/test/jasmine-test/spec/renderTestSpec.js @@ -1,11 +1,25 @@ /*global dust*/ -describe ('Test the basic functionality of dust', function() { +describe ('Render', function() { for (var index = 0; index < coreTests.length; index++) { for (var i = 0; i < coreTests[index].tests.length; i++) { var test = coreTests[index].tests[i]; - it ('RENDER: ' + test.message, render(test)); - it ('STREAM: ' + test.message, stream(test)); - it ('PIPE: ' + test.message, pipe(test)); + it (test.message, render(test)); + } + } +}); +describe ('Stream', function() { + for (var index = 0; index < coreTests.length; index++) { + for (var i = 0; i < coreTests[index].tests.length; i++) { + var test = coreTests[index].tests[i]; + it (test.message, stream(test)); + } + } +}); +describe ('Pipe', function() { + for (var index = 0; index < coreTests.length; index++) { + for (var i = 0; i < coreTests[index].tests.length; i++) { + var test = coreTests[index].tests[i]; + it (test.message, pipe(test)); } } }); @@ -19,7 +33,7 @@ dust.log = function(msg, type) { }; function render(test) { - return function() { + return function(done) { var messageInLog = false; var context; try { @@ -50,25 +64,43 @@ function render(test) { } else { expect(test.expected).toEqual(output); } + done(); }); } catch (error) { expect(test.error || {} ).toEqual(error.message); + done(); } }; } function stream(test) { - return function() { + return function(done) { var output = '', messageInLog = false, log, - flag, context; - runs(function(){ - flag = false; output = ''; log = []; + + function check() { + if (test.error) { + expect(output).toContain(test.error); + } else if(test.log) { + for(var i=0; i 0) { + java.lang.System.exit(1); + } else { + quit(); + } +}; + +env.addReporter(reporter); diff --git a/test/rhino/lib/rhino-1.7R5.jar b/test/rhino/lib/rhino-1.7R5.jar index 4bb7b334..b9dba37c 100644 Binary files a/test/rhino/lib/rhino-1.7R5.jar and b/test/rhino/lib/rhino-1.7R5.jar differ diff --git a/test/rhino/rhinoTest.js b/test/rhino/rhinoTest.js index 2f135e1f..ef2075c5 100644 --- a/test/rhino/rhinoTest.js +++ b/test/rhino/rhinoTest.js @@ -1,41 +1,11 @@ -var setTimeout, - clearTimeout, - setInterval, - clearInterval; - -//set up functions missing in Rhino -(function () { - var timer = new java.util.Timer(); - var counter = 1, - ids = {}; - - setTimeout = function (fn, delay) { - var id = counter++; - ids[id] = new JavaAdapter(java.util.TimerTask,{run: fn}); - timer.schedule(ids[id], delay); - return id; - } - - clearTimeout = function (id) { - ids[id].cancel(); - timer.purge(); - delete ids[id]; - } - - setInterval = function (fn,delay) { - var id = counter++; - ids[id] = new JavaAdapter(java.util.TimerTask,{run: fn}); - timer.schedule(ids[id],delay,delay); - return id; - } - - clearInterval = clearTimeout; -})(); +var window = this; print('Running unit tests with ' + environment['java.class.path']); var requiredFiles = [ - 'node_modules/grunt-contrib-jasmine/vendor/jasmine-1.3.1/jasmine.js', + 'node_modules/grunt-contrib-jasmine/vendor/jasmine-2.0.1/jasmine.js', + 'test/rhino/bootstrap.js', + 'node_modules/ayepromise/ayepromise.js', 'tmp/dust-full.min.js', 'test/jasmine-test/spec/testHelpers.js', 'test/jasmine-test/spec/coreTests.js', @@ -46,36 +16,6 @@ var requiredFiles = [ for(var i = 0; i < requiredFiles.length; i++){ load(requiredFiles[i]); } -var jasmineEnv = jasmine.getEnv(), - reporter = new jasmine.Reporter(); - -jasmineEnv.addReporter(reporter); - -reporter.reportSpecResults = function(spec){ - java.lang.System.out.print('.'); -} - -//set up reporter to print results to console and quit rhino shell -reporter.reportSuiteResults = function (suite) { - print('\n'); - var passed = 0, failed = []; - for(var i = 0; i < suite.specs_.length; i ++) { - if(suite.specs_[i].results_.failedCount > 0) { - print('\tFailed: ' + suite.specs_[i].description); - failed.push(suite.specs_[i]); - } else { - //print('\tPassed: ' + suite.specs_[i].description); - passed++; - } - } - print('Passed ' + passed + ' Failed ' + failed.length + '\n'); - - if(failed.length > 0) { - java.lang.System.exit(1); - } else { - quit(); - } -}; //execute unit tests -jasmineEnv.execute(); +env.execute();