diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..227b4a34 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 140 +block_comment_start = /* +block_comment = * +block_comment_end = */ + +[*.md] +indent_style = space +indent_size = 4 + +[readme.markdown] +indent_size = off +max_line_length = off + +[*.json] +max_line_length = off + +[*.yml] +max_line_length = off + +[Makefile] +max_line_length = off + +[.travis.yml] +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..e93835c3 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "root": true, + "rules": { + "indent": ["error", 4], + }, +} diff --git a/.gitignore b/.gitignore index 3c3629e6..77f05fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -node_modules +# gitignore + +/node_modules + +# Only apps should have lockfiles +yarn.lock +package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml index cc4dba29..1a8c8e67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,38 @@ language: node_js +os: + - linux node_js: - - "0.8" + - "10" + - "9" + - "8" + - "7" + - "6" + - "5" + - "4" + - "iojs" + - "0.12" - "0.10" + - "0.8" +before_install: + - 'case "${TRAVIS_NODE_VERSION}" in 0.*) export NPM_CONFIG_STRICT_SSL=false ;; esac' + - 'nvm install-latest-npm' +install: + - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.9" ]; then nvm install --latest-npm 0.8 && npm install && nvm use "${TRAVIS_NODE_VERSION}"; else npm install; fi;' +script: + - 'if [ -n "${PRETEST-}" ]; then npm run pretest ; fi' + - 'if [ -n "${POSTTEST-}" ]; then npm run posttest ; fi' + - 'if [ -n "${COVERAGE-}" ]; then npm run coverage ; fi' + - 'if [ -n "${TEST-}" ]; then npm run tests-only ; fi' +sudo: false +env: + - TEST=true +matrix: + fast_finish: true + include: + - node_js: "lts/*" + env: PRETEST=true + allow_failures: + - node_js: "9" + - node_js: "7" + - node_js: "5" + - node_js: "iojs" diff --git a/bin/tape b/bin/tape index 332befe8..c479c03b 100755 --- a/bin/tape +++ b/bin/tape @@ -1,8 +1,43 @@ #!/usr/bin/env node -var path = require('path'); -process.argv.slice(2).forEach(function(file) { - require(path.resolve(process.cwd(), file)); +var resolveModule = require('resolve').sync; +var resolvePath = require('path').resolve; +var parseOpts = require('minimist'); +var glob = require('glob'); + +var opts = parseOpts(process.argv.slice(2), { + alias: { r: 'require' }, + string: 'require', + default: { r: [] } + }); + +var cwd = process.cwd(); + +if (typeof opts.require === 'string') { + opts.require = [opts.require]; +} + +opts.require.forEach(function(module) { + if (module) { + /* This check ensures we ignore `-r ""`, trailing `-r`, or + * other silly things the user might (inadvertently) be doing. + */ + require(resolveModule(module, { basedir: cwd })); + } +}); + +opts._.forEach(function (arg) { + // If glob does not match, `files` will be an empty array. + // Note: `glob.sync` may throw an error and crash the node process. + var files = glob.sync(arg); + + if (!Array.isArray(files)) { + throw new TypeError('unknown error: glob.sync did not return an array or throw. Please report this.'); + } + + files.forEach(function (file) { + require(resolvePath(cwd, file)); + }); }); // vim: ft=javascript diff --git a/example/array.js b/example/array.js index d36857d4..bec161f4 100644 --- a/example/array.js +++ b/example/array.js @@ -3,26 +3,26 @@ var test = require('../'); test('array', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/example/fail.js b/example/fail.js index a7bf4444..a0db0b11 100644 --- a/example/fail.js +++ b/example/fail.js @@ -3,26 +3,26 @@ var test = require('../'); test('array', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/example/nested.js b/example/nested.js index 0e233d3f..2a36f25e 100644 --- a/example/nested.js +++ b/example/nested.js @@ -3,35 +3,35 @@ var test = require('../'); test('nested array test', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + t.test('inside test', function (q) { q.plan(2); q.ok(true, 'inside ok'); - + setTimeout(function () { q.ok(true, 'inside delayed'); }, 3000); }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/example/nested_fail.js b/example/nested_fail.js index 3ab5cb3a..ba168c7e 100644 --- a/example/nested_fail.js +++ b/example/nested_fail.js @@ -3,35 +3,35 @@ var test = require('../'); test('nested array test', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + t.test('inside test', function (q) { q.plan(2); q.ok(true); - + setTimeout(function () { q.equal(3, 4); }, 3000); }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/example/not_enough.js b/example/not_enough.js index 13b682be..fffc714f 100644 --- a/example/not_enough.js +++ b/example/not_enough.js @@ -3,26 +3,26 @@ var test = require('../'); test('array', function (t) { t.plan(8); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/example/stream/object.js b/example/stream/object.js new file mode 100644 index 00000000..8f77f0f1 --- /dev/null +++ b/example/stream/object.js @@ -0,0 +1,10 @@ +var test = require('../../'); +var path = require('path'); + +test.createStream({ objectMode: true }).on('data', function (row) { + console.log(JSON.stringify(row)) +}); + +process.argv.slice(2).forEach(function (file) { + require(path.resolve(file)); +}); diff --git a/example/stream/tap.js b/example/stream/tap.js new file mode 100644 index 00000000..9ea9ff74 --- /dev/null +++ b/example/stream/tap.js @@ -0,0 +1,8 @@ +var test = require('../../'); +var path = require('path'); + +test.createStream().pipe(process.stdout); + +process.argv.slice(2).forEach(function (file) { + require(path.resolve(file)); +}); diff --git a/example/stream/test/x.js b/example/stream/test/x.js new file mode 100644 index 00000000..7dbb98ad --- /dev/null +++ b/example/stream/test/x.js @@ -0,0 +1,5 @@ +var test = require('../../../'); +test(function (t) { + t.plan(1); + t.equal('beep', 'boop'); +}); diff --git a/example/stream/test/y.js b/example/stream/test/y.js new file mode 100644 index 00000000..28606d51 --- /dev/null +++ b/example/stream/test/y.js @@ -0,0 +1,11 @@ +var test = require('../../../'); +test(function (t) { + t.plan(2); + t.equal(1+1, 2); + t.ok(true); +}); + +test('wheee', function (t) { + t.ok(true); + t.end(); +}); diff --git a/example/throw.js b/example/throw.js index 9a69ec0f..5fa8b94c 100644 --- a/example/throw.js +++ b/example/throw.js @@ -3,7 +3,7 @@ var test = require('../'); test('throw', function (t) { t.plan(2); - + setTimeout(function () { throw new Error('doom'); }, 100); diff --git a/example/timing.js b/example/timing.js index 0268dc78..614c1441 100644 --- a/example/timing.js +++ b/example/timing.js @@ -2,10 +2,10 @@ var test = require('../'); test('timing test', function (t) { t.plan(2); - + t.equal(typeof Date.now, 'function'); var start = new Date; - + setTimeout(function () { t.equal(new Date - start, 100); }, 100); diff --git a/example/too_many.js b/example/too_many.js index ee285fba..cdcb5ee9 100644 --- a/example/too_many.js +++ b/example/too_many.js @@ -3,26 +3,26 @@ var test = require('../'); test('array', function (t) { t.plan(3); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/index.js b/index.js index b39936df..48528eb9 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,11 @@ +var defined = require('defined'); var createDefaultStream = require('./lib/default_stream'); var Test = require('./lib/test'); -var createResultStream = require('./lib/results'); +var createResult = require('./lib/results'); +var through = require('through'); var canEmitExit = typeof process !== 'undefined' && process - && typeof process.on === 'function' + && typeof process.on === 'function' && process.browser !== true ; var canExit = typeof process !== 'undefined' && process && typeof process.exit === 'function' @@ -17,110 +19,141 @@ var nextTick = typeof setImmediate !== 'undefined' exports = module.exports = (function () { var harness; var lazyLoad = function () { - if (!harness) harness = createExitHarness(); - - return harness.apply(this, arguments); + return getHarness().apply(this, arguments); }; lazyLoad.only = function () { - if (!harness) harness = createExitHarness(); + return getHarness().only.apply(this, arguments); + }; - return harness.only.apply(this, arguments); - } + lazyLoad.createStream = function (opts) { + if (!opts) opts = {}; + if (!harness) { + var output = through(); + getHarness({ stream: output, objectMode: opts.objectMode }); + return output; + } + return harness.createStream(opts); + }; + + lazyLoad.onFinish = function () { + return getHarness().onFinish.apply(this, arguments); + }; + + lazyLoad.onFailure = function() { + return getHarness().onFailure.apply(this, arguments); + }; + + lazyLoad.getHarness = getHarness return lazyLoad + + function getHarness (opts) { + if (!opts) opts = {}; + opts.autoclose = !canEmitExit; + if (!harness) harness = createExitHarness(opts); + return harness; + } })(); function createExitHarness (conf) { if (!conf) conf = {}; - var harness = createHarness(); - var stream = harness.createStream(); - stream.pipe(createDefaultStream()); - + var harness = createHarness({ + autoclose: defined(conf.autoclose, false) + }); + + var stream = harness.createStream({ objectMode: conf.objectMode }); + var es = stream.pipe(conf.stream || createDefaultStream()); + if (canEmitExit) { + es.on('error', function (err) { harness._exitCode = 1 }); + } + var ended = false; stream.on('end', function () { ended = true }); - + if (conf.exit === false) return harness; if (!canEmitExit || !canExit) return harness; - - var _error; - process.on('uncaughtException', function (err) { - _error = err - - throw err - }) + var inErrorState = false; process.on('exit', function (code) { - if (_error) { + // let the process exit cleanly. + if (code !== 0) { return } if (!ended) { + var only = harness._results._only; for (var i = 0; i < harness._tests.length; i++) { var t = harness._tests[i]; + if (only && t !== only) continue; t._exit(); } } + harness.close(); process.exit(code || harness._exitCode); }); + return harness; } exports.createHarness = createHarness; exports.Test = Test; exports.test = exports; // tap compat +exports.test.skip = Test.skip; var exitInterval; function createHarness (conf_) { - var results; - + if (!conf_) conf_ = {}; + var results = createResult(); + if (conf_.autoclose !== false) { + results.once('done', function () { results.close() }); + } + var test = function (name, conf, cb) { - if (!results) { - results = createResultStream(); - results.pause(); - } - var t = new Test(name, conf, cb); test._tests.push(t); - + (function inspectCode (st) { st.on('test', function sub (st_) { inspectCode(st_); }); st.on('result', function (r) { - if (!r.ok) test._exitCode = 1 + if (!r.ok && typeof r !== 'string') test._exitCode = 1 }); })(t); - + results.push(t); return t; }; - + test._results = results; + test._tests = []; - - test.createStream = function () { - if (!results) results = createResultStream(); - - var _pause = results.pause; - var paused = false; - results.pause = function () { paused = true }; - - nextTick(function () { - if (!paused) results.resume(); - }); - return results; + + test.createStream = function (opts) { + return results.createStream(opts); }; - + + test.onFinish = function (cb) { + results.on('done', cb); + }; + + test.onFailure = function (cb) { + results.on('fail', cb); + }; + var only = false; - test.only = function (name) { + test.only = function () { if (only) throw new Error('there can only be one only test'); - results.only(name); only = true; - return test.apply(null, arguments); + var t = test.apply(null, arguments); + results.only(t); + return t; }; test._exitCode = 0; - + + test.close = function () { results.close() }; + return test; } diff --git a/lib/default_stream.js b/lib/default_stream.js index 0e3861fb..8e9f4c76 100644 --- a/lib/default_stream.js +++ b/lib/default_stream.js @@ -1,30 +1,30 @@ -var Stream = require('stream'); +var through = require('through'); +var fs = require('fs'); module.exports = function () { - var out = new Stream; - out.writable = true; - var buffered = ''; - - out.write = function (buf) { - var s = buffered + String(buf); - var lines = s.split('\n'); - for (var i = 0; i < lines.length - 1; i++) { - console.log(lines[i]); + var line = ''; + var stream = through(write, flush); + return stream; + + function write (buf) { + for (var i = 0; i < buf.length; i++) { + var c = typeof buf === 'string' + ? buf.charAt(i) + : String.fromCharCode(buf[i]) + ; + if (c === '\n') flush(); + else line += c; + } + } + + function flush () { + if (fs.writeSync && /^win/.test(process.platform)) { + try { fs.writeSync(1, line + '\n'); } + catch (e) { stream.emit('error', e) } + } else { + try { console.log(line) } + catch (e) { stream.emit('error', e) } } - buffered = lines[i]; - }; - - out.destroy = function () { - out.writable = false; - out.emit('close'); - }; - - out.end = function (msg) { - if (msg !== undefined) out.write(msg); - if (buffered) console.log(buffered); - out.writable = false; - out.emit('close'); - }; - - return out; + line = ''; + } }; diff --git a/lib/results.js b/lib/results.js index 0bbde297..cfe6c022 100644 --- a/lib/results.js +++ b/lib/results.js @@ -1,85 +1,105 @@ -var Stream = require('stream'); -var json = typeof JSON === 'object' ? JSON : require('jsonify'); +var defined = require('defined'); +var EventEmitter = require('events').EventEmitter; +var inherits = require('inherits'); var through = require('through'); +var resumer = require('resumer'); +var inspect = require('object-inspect'); +var bind = require('function-bind'); +var has = require('has'); +var regexpTest = bind.call(Function.call, RegExp.prototype.test); +var yamlIndicators = /\:|\-|\?/; var nextTick = typeof setImmediate !== 'undefined' ? setImmediate : process.nextTick ; -module.exports = function () { - var output = through(); - output.pause(); - output.queue('TAP version 13\n'); - - var results = new Results(output); - output.push = function (t) { results.push(t) }; - - output.only = function (name) { - results.only = name; - }; - - nextTick(function next () { - var t = getNextTest(results); - if (!t && results.running) return; - if (!t) return results.close(); - t.run(); - }); - - return output; -}; +module.exports = Results; +inherits(Results, EventEmitter); -function Results (stream) { +function Results () { + if (!(this instanceof Results)) return new Results; this.count = 0; this.fail = 0; this.pass = 0; - this.stream = stream; + this.todo = 0; + this._stream = through(); this.tests = []; - this.running = 0; + this._only = null; + this._isRunning = false; } -Results.prototype.push = function (t, parentT) { +Results.prototype.createStream = function (opts) { + if (!opts) opts = {}; var self = this; - var write = function (s) { self.stream.queue(s) }; - t.once('prerun', function () { - if (self.only && self.only !== t.name && !parentT) { - var nt = getNextTest(self); - if (nt) nt.run() - else self.close(); - return; - } - - self.running ++; - write('# ' + t.name + '\n'); - }); - if (parentT) { - var ix = self.tests.indexOf(parentT); - if (ix >= 0) self.tests.splice(ix, 0, t); + var output, testId = 0; + if (opts.objectMode) { + output = through(); + self.on('_push', function ontest (t, extra) { + if (!extra) extra = {}; + var id = testId++; + t.once('prerun', function () { + var row = { + type: 'test', + name: t.name, + id: id + }; + if (has(extra, 'parent')) { + row.parent = extra.parent; + } + output.queue(row); + }); + t.on('test', function (st) { + ontest(st, { parent: id }); + }); + t.on('result', function (res) { + res.test = id; + res.type = 'assert'; + output.queue(res); + }); + t.on('end', function () { + output.queue({ type: 'end', test: id }); + }); + }); + self.on('done', function () { output.queue(null) }); + } else { + output = resumer(); + output.queue('TAP version 13\n'); + self._stream.pipe(output); } - else self.tests.push(t); - - var plan; - t.on('plan', function (n) { plan = n }); - - var subtests = 0; - - t.on('test', function (st) { - subtests ++; - st.on('end', function () { - subtests --; - if (subtests === 1) nextTick(function () { st.run() }); - else if (subtests === 0 && !t.ended) { - t.end(); + + if (!this._isRunning) { + this._isRunning = true; + nextTick(function next() { + var t; + while (t = getNextTest(self)) { + t.run(); + if (!t.ended) return t.once('end', function(){ nextTick(next); }); } + self.emit('done'); }); - self.push(st, t); - if (subtests === 1) { - if (plan === undefined) st.run(); - else nextTick(function () { - st.run(); - }); - } + } + + return output; +}; + +Results.prototype.push = function (t) { + var self = this; + self.tests.push(t); + self._watch(t); + self.emit('_push', t); +}; + +Results.prototype.only = function (t) { + this._only = t; +}; + +Results.prototype._watch = function (t) { + var self = this; + var write = function (s) { self._stream.queue(s) }; + t.once('prerun', function () { + write('# ' + t.name + '\n'); }); - + t.on('result', function (res) { if (typeof res === 'string') { write('# ' + res + '\n'); @@ -87,124 +107,94 @@ Results.prototype.push = function (t, parentT) { } write(encodeResult(res, self.count + 1)); self.count ++; - - if (res.ok) self.pass ++ - else self.fail ++ - }); - - t.once('end', function () { - if (t._skip) { - var nt = getNextTest(self); - if (nt) nt.run(); - else self.close(); - return; - } - - self.running --; - if (subtests !== 0) return; - - if (self.running === 0 && self.tests.length) { - var nt = getNextTest(self); - if (nt) nt.run(); - else self.close(); - } - else if (self.running === 0) { - self.close(); + + if (res.ok || res.todo) self.pass ++ + else { + self.fail ++; + self.emit('fail'); } }); + + t.on('test', function (st) { self._watch(st) }); }; Results.prototype.close = function () { var self = this; - if (self.closed) self.stream.emit('error', new Error('ALREADY CLOSED')); + if (self.closed) self._stream.emit('error', new Error('ALREADY CLOSED')); self.closed = true; - var write = function (s) { self.stream.queue(s) }; - + var write = function (s) { self._stream.queue(s) }; + write('\n1..' + self.count + '\n'); write('# tests ' + self.count + '\n'); - write('# pass ' + self.pass + '\n'); - if (self.fail) write('# fail ' + self.fail + '\n') - else write('\n# ok\n') - - self.stream.queue(null); + write('# pass ' + (self.pass + self.todo) + '\n'); + if (self.todo) write('# todo ' + self.todo + '\n'); + if (self.fail) write('# fail ' + self.fail + '\n'); + else write('\n# ok\n'); + + self._stream.queue(null); }; function encodeResult (res, count) { var output = ''; output += (res.ok ? 'ok ' : 'not ok ') + count; - output += res.name ? ' ' + res.name.replace(/\s+/g, ' ') : ''; - + output += res.name ? ' ' + res.name.toString().replace(/\s+/g, ' ') : ''; + if (res.skip) output += ' # SKIP'; else if (res.todo) output += ' # TODO'; - + output += '\n'; if (res.ok) return output; - + var outer = ' '; var inner = outer + ' '; output += outer + '---\n'; output += inner + 'operator: ' + res.operator + '\n'; - - var ex = json.stringify(res.expected, getSerialize()) || ''; - var ac = json.stringify(res.actual, getSerialize()) || ''; - - if (Math.max(ex.length, ac.length) > 65) { - output += inner + 'expected:\n' + inner + ' ' + ex + '\n'; - output += inner + 'actual:\n' + inner + ' ' + ac + '\n'; - } - else { - output += inner + 'expected: ' + ex + '\n'; - output += inner + 'actual: ' + ac + '\n'; + + if (has(res, 'expected') || has(res, 'actual')) { + var ex = inspect(res.expected, {depth: res.objectPrintDepth}); + var ac = inspect(res.actual, {depth: res.objectPrintDepth}); + + if (Math.max(ex.length, ac.length) > 65 || invalidYaml(ex) || invalidYaml(ac)) { + output += inner + 'expected: |-\n' + inner + ' ' + ex + '\n'; + output += inner + 'actual: |-\n' + inner + ' ' + ac + '\n'; + } else { + output += inner + 'expected: ' + ex + '\n'; + output += inner + 'actual: ' + ac + '\n'; + } } if (res.at) { output += inner + 'at: ' + res.at + '\n'; } - if (res.operator === 'error' && res.actual && res.actual.stack) { - var lines = String(res.actual.stack).split('\n'); - output += inner + 'stack:\n'; - output += inner + ' ' + lines[0] + '\n'; - for (var i = 1; i < lines.length; i++) { - output += inner + lines[i] + '\n'; + + var actualStack = res.actual && (typeof res.actual === 'object' || typeof res.actual === 'function') ? res.actual.stack : undefined; + var errorStack = res.error && res.error.stack; + var stack = defined(actualStack, errorStack); + if (stack) { + var lines = String(stack).split('\n'); + output += inner + 'stack: |-\n'; + for (var i = 0; i < lines.length; i++) { + output += inner + ' ' + lines[i] + '\n'; } } - + output += outer + '...\n'; return output; } -function getSerialize () { - var seen = []; - - return function (key, value) { - var ret = value; - if (typeof value === 'object' && value) { - var found = false; - for (var i = 0; i < seen.length; i++) { - if (seen[i] === value) { - found = true - break; - } - } - - if (found) ret = '[Circular]' - else seen.push(value) - } - return ret; - }; -} - -function getNextTest(results) { - if (!results.only) { +function getNextTest (results) { + if (!results._only) { return results.tests.shift(); } do { var t = results.tests.shift(); - if (!t) { - return null; - } - if (results.only === t.name) { + if (!t) continue; + if (results._only === t) { return t; } } while (results.tests.length !== 0) } + +function invalidYaml (str) { + return regexpTest(yamlIndicators, str); +} diff --git a/lib/test.js b/lib/test.js index 03e63554..d594ba46 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,72 +1,137 @@ -var Stream = require('stream'); var deepEqual = require('deep-equal'); var defined = require('defined'); var path = require('path'); -var inherits = require('util').inherits; +var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; +var has = require('has'); +var trim = require('string.prototype.trim'); +var bind = require('function-bind'); +var forEach = require('for-each'); +var isEnumerable = bind.call(Function.call, Object.prototype.propertyIsEnumerable); +var toLowerCase = bind.call(Function.call, String.prototype.toLowerCase); module.exports = Test; var nextTick = typeof setImmediate !== 'undefined' ? setImmediate - : process.nextTick -; + : process.nextTick; +var safeSetTimeout = setTimeout; +var safeClearTimeout = clearTimeout; inherits(Test, EventEmitter); -function Test (name_, opts_, cb_) { - var self = this; +var getTestArgs = function (name_, opts_, cb_) { var name = '(anonymous)'; var opts = {}; var cb; - + for (var i = 0; i < arguments.length; i++) { - switch (typeof arguments[i]) { - case 'string': - name = arguments[i]; - break; - case 'object': - opts = arguments[i] || opts; - break; - case 'function': - cb = arguments[i]; + var arg = arguments[i]; + var t = typeof arg; + if (t === 'string') { + name = arg; + } else if (t === 'object') { + opts = arg || opts; + } else if (t === 'function') { + cb = arg; } } - + return { name: name, opts: opts, cb: cb }; +}; + +var runProgeny = function () { + var self = this; + if (this._progeny.length) { + var t = this._progeny.shift(); + t.on('end', function () { runProgeny.call(self) }); + nextTick(function () { + t.run(); + }); + return; + } + if (this.calledEnd || this._plan) { + this._end(); + } +}; + +function Test (name_, opts_, cb_) { + if (! (this instanceof Test)) { + return new Test(name_, opts_, cb_); + } + + var args = getTestArgs(name_, opts_, cb_); + this.readable = true; - this.name = name || '(anonymous)'; + this.name = args.name || '(anonymous)'; this.assertCount = 0; - this._skip = opts.skip || false; + this.pendingCount = 0; + this._skip = args.opts.skip || false; + this._todo = args.opts.todo || false; + this._timeout = args.opts.timeout; this._plan = undefined; - this._cb = cb; + this._cb = args.cb; this._progeny = []; this._ok = true; + var depthEnvVar = process.env.NODE_TAPE_OBJECT_PRINT_DEPTH; + if (args.opts.objectPrintDepth) { + this._objectPrintDepth = args.opts.objectPrintDepth; + } else if (depthEnvVar) { + if (toLowerCase(depthEnvVar) === 'infinity') { + this._objectPrintDepth = Infinity; + } else { + this._objectPrintDepth = depthEnvVar; + } + } else { + this._objectPrintDepth = 5; + } + + for (var prop in this) { + this[prop] = (function bind(self, val) { + if (typeof val === 'function') { + return function bound() { + return val.apply(self, arguments); + }; + } + return val; + })(this, this[prop]); + } } Test.prototype.run = function () { if (this._skip) { - return this.end(); + this.comment('SKIP ' + this.name); } - this.emit('prerun'); - try { - this._cb(this); + if (!this._cb || this._skip) { + return this._end(); } - catch (err) { - this.error(err); - this.end(); - return; + if (this._timeout != null) { + this.timeoutAfter(this._timeout); } + this.emit('prerun'); + this._cb(this); this.emit('run'); }; Test.prototype.test = function (name, opts, cb) { + var self = this; var t = new Test(name, opts, cb); this._progeny.push(t); + this.pendingCount++; this.emit('test', t); + t.on('prerun', function () { + self.assertCount++; + }); + + if (!this._pendingAsserts()) { + runProgeny.call(this); + } }; Test.prototype.comment = function (msg) { - this.emit('result', msg.trim().replace(/^#\s*/, '')); + var that = this; + forEach(trim(msg).split('\n'), function (aMsg) { + that.emit('result', trim(aMsg).replace(/^#\s*/, '')); + }); }; Test.prototype.plan = function (n) { @@ -74,17 +139,34 @@ Test.prototype.plan = function (n) { this.emit('plan', n); }; -Test.prototype.end = function () { +Test.prototype.timeoutAfter = function (ms) { + if (!ms) throw new Error('timeoutAfter requires a timespan'); var self = this; - if (this._progeny.length) { - var t = this._progeny.shift(); - t.on('end', function () { self.end() }); - return; + var timeout = safeSetTimeout(function () { + self.fail('test timed out after ' + ms + 'ms'); + self.end(); + }, ms); + this.once('end', function () { + safeClearTimeout(timeout); + }); +} + +Test.prototype.end = function (err) { + if (arguments.length >= 1 && !!err) { + this.ifError(err); } - + + if (this.calledEnd) { + this.fail('.end() called twice'); + } + this.calledEnd = true; + runProgeny.call(this); +}; + +Test.prototype._end = function (err) { if (!this.ended) this.emit('end'); - if (this._plan !== undefined && - !this._planError && this.assertCount !== this._plan) { + var pendingAsserts = this._pendingAsserts(); + if (!this._planError && this._plan !== undefined && pendingAsserts) { this._planError = true; this.fail('plan != count', { expected : this._plan, @@ -103,76 +185,128 @@ Test.prototype._exit = function () { actual : this.assertCount, exiting : true }); - } - else if (!this.ended) { + } else if (!this.ended) { this.fail('test exited without ending', { exiting: true }); } }; +Test.prototype._pendingAsserts = function () { + if (this._plan === undefined) { + return 1; + } + return this._plan - (this._progeny.length + this.assertCount); +}; + Test.prototype._assert = function assert (ok, opts) { var self = this; var extra = opts.extra || {}; - + var res = { - id : self.assertCount ++, - ok : Boolean(ok), - skip : defined(extra.skip, opts.skip), - name : defined(extra.message, opts.message, '(unnamed assert)'), - operator : defined(extra.operator, opts.operator), - actual : defined(extra.actual, opts.actual), - expected : defined(extra.expected, opts.expected) + id: self.assertCount++, + ok: Boolean(ok), + skip: defined(extra.skip, opts.skip), + todo: defined(extra.todo, opts.todo, self._todo), + name: defined(extra.message, opts.message, '(unnamed assert)'), + operator: defined(extra.operator, opts.operator), + objectPrintDepth: self._objectPrintDepth }; + if (has(opts, 'actual') || has(extra, 'actual')) { + res.actual = defined(extra.actual, opts.actual); + } + if (has(opts, 'expected') || has(extra, 'expected')) { + res.expected = defined(extra.expected, opts.expected); + } this._ok = Boolean(this._ok && ok); - - if (!ok) { + + if (!ok && !res.todo) { res.error = defined(extra.error, opts.error, new Error(res.name)); } - - var e = new Error('exception'); - var err = (e.stack || '').split('\n'); - var dir = path.dirname(__dirname) + '/'; - - for (var i = 0; i < err.length; i++) { - var m = /^\s*\bat\s+(.+)/.exec(err[i]); - if (!m) continue; - - var s = m[1].split(/\s+/); - var filem = /(\/[^:\s]+:(\d+)(?::(\d+))?)/.exec(s[1]); - if (!filem) { - filem = /(\/[^:\s]+:(\d+)(?::(\d+))?)/.exec(s[3]); - - if (!filem) continue; + + if (!ok) { + var e = new Error('exception'); + var err = (e.stack || '').split('\n'); + var dir = __dirname + path.sep; + + for (var i = 0; i < err.length; i++) { + /* + Stack trace lines may resemble one of the following. We need + to should correctly extract a function name (if any) and + path / line no. for each line. + + at myFunction (/path/to/file.js:123:45) + at myFunction (/path/to/file.other-ext:123:45) + at myFunction (/path to/file.js:123:45) + at myFunction (C:\path\to\file.js:123:45) + at myFunction (/path/to/file.js:123) + at Test. (/path/to/file.js:123:45) + at Test.bound [as run] (/path/to/file.js:123:45) + at /path/to/file.js:123:45 + + Regex has three parts. First is non-capturing group for 'at ' + (plus anything preceding it). + + /^(?:[^\s]*\s*\bat\s+)/ + + Second captures function call description (optional). This is + not necessarily a valid JS function name, but just what the + stack trace is using to represent a function call. It may look + like `` or 'Test.bound [as run]'. + + For our purposes, we assume that, if there is a function + name, it's everything leading up to the first open + parentheses (trimmed) before our pathname. + + /(?:(.*)\s+\()?/ + + Last part captures file path plus line no (and optional + column no). + + /((?:\/|[a-zA-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)/ + */ + var re = /^(?:[^\s]*\s*\bat\s+)(?:(.*)\s+\()?((?:\/|[a-zA-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)/ + var m = re.exec(err[i]); + + if (!m) { + continue; + } + + var callDescription = m[1] || ''; + var filePath = m[2]; + + if (filePath.slice(0, dir.length) === dir) { + continue; + } + + // Function call description may not (just) be a function name. + // Try to extract function name by looking at first "word" only. + res.functionName = callDescription.split(/\s+/)[0] + res.file = filePath; + res.line = Number(m[3]); + if (m[4]) res.column = Number(m[4]); + + res.at = callDescription + ' (' + filePath + ')'; + break; } - - if (filem[1].slice(0, dir.length) === dir) continue; - - res.functionName = s[0]; - res.file = filem[1]; - res.line = Number(filem[2]); - if (filem[3]) res.column = filem[3]; - - res.at = m[1]; - break; } - + self.emit('result', res); - - if (self._plan === self.assertCount && extra.exiting) { - if (!self.ended) self.end(); - } - else if (self._plan === self.assertCount) { - nextTick(function () { - if (!self.ended) self.end(); - }); + + var pendingAsserts = self._pendingAsserts(); + if (!pendingAsserts) { + if (extra.exiting) { + self._end(); + } else { + runProgeny.call(self); + } } - - if (!self._planError && self.assertCount > self._plan) { + + if (!self._planError && pendingAsserts < 0) { self._planError = true; self.fail('plan != count', { expected : self._plan, - actual : self.assertCount + actual : self._plan - pendingAsserts }); } }; @@ -202,61 +336,74 @@ Test.prototype.skip = function (msg, extra) { }); }; -Test.prototype.ok -= Test.prototype['true'] -= Test.prototype.assert -= function (value, msg, extra) { +function assert(value, msg, extra) { this._assert(value, { - message : msg, + message : defined(msg, 'should be truthy'), operator : 'ok', expected : true, actual : value, extra : extra }); -}; +} +Test.prototype.ok += Test.prototype['true'] += Test.prototype.assert += assert; -Test.prototype.notOk -= Test.prototype['false'] -= Test.prototype.notok -= function (value, msg, extra) { +function notOK(value, msg, extra) { this._assert(!value, { - message : msg, + message : defined(msg, 'should be falsy'), operator : 'notOk', expected : false, actual : value, extra : extra }); -}; +} +Test.prototype.notOk += Test.prototype['false'] += Test.prototype.notok += notOK; -Test.prototype.error -= Test.prototype.ifError -= Test.prototype.ifErr -= Test.prototype.iferror -= function (err, msg, extra) { +function error(err, msg, extra) { this._assert(!err, { message : defined(msg, String(err)), operator : 'error', actual : err, extra : extra }); -}; +} +Test.prototype.error += Test.prototype.ifError += Test.prototype.ifErr += Test.prototype.iferror += error; +function equal(a, b, msg, extra) { + this._assert(a === b, { + message : defined(msg, 'should be equal'), + operator : 'equal', + actual : a, + expected : b, + extra : extra + }); +} Test.prototype.equal = Test.prototype.equals = Test.prototype.isEqual = Test.prototype.is = Test.prototype.strictEqual = Test.prototype.strictEquals -= function (a, b, msg, extra) { - this._assert(a === b, { - message : defined(msg, 'should be equal'), - operator : 'equal', += equal; + +function notEqual(a, b, msg, extra) { + this._assert(a !== b, { + message : defined(msg, 'should not be equal'), + operator : 'notEqual', actual : a, expected : b, extra : extra }); -}; - +} Test.prototype.notEqual = Test.prototype.notEquals = Test.prototype.notStrictEqual @@ -266,33 +413,48 @@ Test.prototype.notEqual = Test.prototype.not = Test.prototype.doesNotEqual = Test.prototype.isInequal -= function (a, b, msg, extra) { - this._assert(a !== b, { - message : defined(msg, 'should not be equal'), - operator : 'notEqual', += notEqual; + +function tapeDeepEqual(a, b, msg, extra) { + this._assert(deepEqual(a, b, { strict: true }), { + message : defined(msg, 'should be equivalent'), + operator : 'deepEqual', actual : a, - notExpected : b, + expected : b, extra : extra }); -}; - +} Test.prototype.deepEqual = Test.prototype.deepEquals = Test.prototype.isEquivalent -= Test.prototype.looseEqual -= Test.prototype.looseEquals = Test.prototype.same -= function (a, b, msg, extra) { += tapeDeepEqual; + +function deepLooseEqual(a, b, msg, extra) { this._assert(deepEqual(a, b), { message : defined(msg, 'should be equivalent'), - operator : 'deepEqual', + operator : 'deepLooseEqual', actual : a, expected : b, extra : extra }); -}; +} +Test.prototype.deepLooseEqual += Test.prototype.looseEqual += Test.prototype.looseEquals += deepLooseEqual; +function notDeepEqual(a, b, msg, extra) { + this._assert(!deepEqual(a, b, { strict: true }), { + message : defined(msg, 'should not be equivalent'), + operator : 'notDeepEqual', + actual : a, + expected : b, + extra : extra + }); +} Test.prototype.notDeepEqual += Test.prototype.notDeepEquals = Test.prototype.notEquivalent = Test.prototype.notDeeply = Test.prototype.notSame @@ -300,30 +462,39 @@ Test.prototype.notDeepEqual = Test.prototype.isNotDeeply = Test.prototype.isNotEquivalent = Test.prototype.isInequivalent -= function (a, b, msg, extra) { += notDeepEqual; + +function notDeepLooseEqual(a, b, msg, extra) { this._assert(!deepEqual(a, b), { - message : defined(msg, 'should not be equivalent'), - operator : 'notDeepEqual', + message : defined(msg, 'should be equivalent'), + operator : 'notDeepLooseEqual', actual : a, - notExpected : b, + expected : b, extra : extra }); -}; +} +Test.prototype.notDeepLooseEqual += Test.prototype.notLooseEqual += Test.prototype.notLooseEquals += notDeepLooseEqual; Test.prototype['throws'] = function (fn, expected, msg, extra) { if (typeof expected === 'string') { msg = expected; expected = undefined; } + var caught = undefined; + try { fn(); - } - catch (err) { + } catch (err) { caught = { error : err }; - var message = err.message; - delete err.message; - err.message = message; + if ((err != null) && (!isEnumerable(err, 'message') || !has(err, 'message'))) { + var message = err.message; + delete err.message; + err.message = message; + } } var passed = caught; @@ -333,6 +504,11 @@ Test.prototype['throws'] = function (fn, expected, msg, extra) { expected = String(expected); } + if (typeof expected === 'function' && caught) { + passed = caught.error instanceof expected; + caught.error = caught.error.constructor; + } + this._assert(typeof fn === 'function' && passed, { message : defined(msg, 'should throw'), operator : 'throws', @@ -356,7 +532,7 @@ Test.prototype.doesNotThrow = function (fn, expected, msg, extra) { caught = { error : err }; } this._assert(!caught, { - message : defined(msg, 'should throw'), + message : defined(msg, 'should not throw'), operator : 'throws', actual : caught && caught.error, expected : expected, @@ -365,4 +541,10 @@ Test.prototype.doesNotThrow = function (fn, expected, msg, extra) { }); }; +Test.skip = function (name_, _opts, _cb) { + var args = getTestArgs.apply(null, arguments); + args.opts.skip = true; + return Test(args.name, args.opts, args.cb); +}; + // vim: set softtabstop=4 shiftwidth=4: diff --git a/package.json b/package.json index 6d218a90..c80629d9 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,72 @@ { - "name" : "tape", - "version" : "1.1.2", - "description" : "tap-producing test harness for node and browsers", - "main" : "index.js", - "bin" : "./bin/tape", - "directories" : { - "example" : "example", - "test" : "test" + "name": "tape", + "version": "4.9.2", + "description": "tap-producing test harness for node and browsers", + "main": "index.js", + "bin": "./bin/tape", + "directories": { + "example": "example", + "test": "test" }, - "dependencies" : { - "jsonify" : "~0.0.0", - "deep-equal" : "~0.0.0", - "defined" : "~0.0.0", - "through": "~2.3.4" + "dependencies": { + "deep-equal": "~1.0.1", + "defined": "~1.0.0", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.3", + "has": "~1.0.3", + "inherits": "~2.0.3", + "minimist": "~1.2.0", + "object-inspect": "~1.6.0", + "resolve": "~1.7.1", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.1.2", + "through": "~2.3.8" }, - "devDependencies" : { - "tap" : "~0.3.0", - "falafel" : "~0.1.4", - "concat-stream": "~1.5.1" + "devDependencies": { + "concat-stream": "^1.6.2", + "eclint": "^2.8.1", + "eslint": "^5.13.0", + "falafel": "^2.1.0", + "js-yaml": "^3.12.1", + "tap": "^8.0.1", + "tap-parser": "^3.0.5" }, - "scripts" : { - "test" : "tap test/*.js" + "scripts": { + "prelint": "eclint check", + "lint": "eslint .", + "pretest": "npm run lint", + "test": "npm run tests-only", + "tests-only": "tap test/*.js" }, - "testling" : { - "files" : "test/browser/*.js", - "browsers" : [ + "testling": { + "files": "test/browser/*.js", + "browsers": [ "ie/6..latest", "chrome/20..latest", "firefox/10..latest", "safari/latest", "opera/11.0..latest", - "iphone/6", "ipad/6" + "iphone/6", + "ipad/6" ] }, - "repository" : { - "type" : "git", - "url" : "git://github.com/substack/tape.git" + "repository": { + "type": "git", + "url": "git://github.com/substack/tape.git" }, - "homepage" : "https://github.com/substack/tape", - "keywords" : [ + "homepage": "https://github.com/substack/tape", + "keywords": [ "tap", "test", "harness", "assert", "browser" ], - "author" : { - "name" : "James Halliday", - "email" : "mail@substack.net", - "url" : "http://substack.net" + "author": { + "name": "James Halliday", + "email": "mail@substack.net", + "url": "http://substack.net" }, - "license" : "MIT" + "license": "MIT" } diff --git a/readme.markdown b/readme.markdown index e3ae322a..3306790c 100644 --- a/readme.markdown +++ b/readme.markdown @@ -4,15 +4,9 @@ tap-producing test harness for node and browsers [![browser support](https://ci.testling.com/substack/tape.png)](http://ci.testling.com/substack/tape) -[![build status](https://secure.travis-ci.org/substack/tape.png)](http://travis-ci.org/substack/tape) +[![build status](https://secure.travis-ci.org/substack/tape.svg?branch=master)](http://travis-ci.org/substack/tape) -![tape](http://substack.net/images/tape_drive.png) - -# browser compatibility - -chrome, firefox, opera, safari, IE6, IE7, IE8, IE9 - -using browserify@1.16.5 +![tape](https://web.archive.org/web/20170612184731if_/http://substack.net/images/tape_drive.png) # example @@ -21,10 +15,10 @@ var test = require('tape'); test('timing test', function (t) { t.plan(2); - + t.equal(typeof Date.now, 'function'); var start = Date.now(); - + setTimeout(function () { t.equal(Date.now() - start, 100); }, 100); @@ -49,6 +43,96 @@ not ok 2 should be equal # fail 1 ``` +# usage + +You always need to `require('tape')` in test files. You can run the tests by +usual node means (`require('test-file.js')` or `node test-file.js`). You can +also run tests using the `tape` binary to utilize globbing, on Windows for +example: + +```sh +$ tape tests/**/*.js +``` + +`tape`'s arguments are passed to the +[`glob`](https://www.npmjs.com/package/glob) module. If you want `glob` to +perform the expansion on a system where the shell performs such expansion, quote +the arguments as necessary: + +```sh +$ tape 'tests/**/*.js' +$ tape "tests/**/*.js" +``` + +## Preloading modules + +Additionally, it is possible to make `tape` load one or more modules before running any tests, by using the `-r` or `--require` flag. Here's an example that loads [babel-register](http://babeljs.io/docs/usage/require/) before running any tests, to allow for JIT compilation: + +```sh +$ tape -r babel-register tests/**/*.js +``` + +Depending on the module you're loading, you may be able to parameterize it using environment variables or auxiliary files. Babel, for instance, will load options from [`.babelrc`](http://babeljs.io/docs/usage/babelrc/) at runtime. + +The `-r` flag behaves exactly like node's `require`, and uses the same module resolution algorithm. This means that if you need to load local modules, you have to prepend their path with `./` or `../` accordingly. + +For example: + +```sh +$ tape -r ./my/local/module tests/**/*.js +``` + +Please note that all modules loaded using the `-r` flag will run *before* any tests, regardless of when they are specified. For example, `tape -r a b -r c` will actually load `a` and `c` *before* loading `b`, since they are flagged as required modules. + +# things that go well with tape + +tape maintains a fairly minimal core. Additional features are usually added by using another module alongside tape. + +## pretty reporters + +The default TAP output is good for machines and humans that are robots. + +If you want a more colorful / pretty output there are lots of modules on npm +that will output something pretty if you pipe TAP into them: + +- [tap-spec](https://github.com/scottcorgan/tap-spec) +- [tap-dot](https://github.com/scottcorgan/tap-dot) +- [faucet](https://github.com/substack/faucet) +- [tap-bail](https://github.com/juliangruber/tap-bail) +- [tap-browser-color](https://github.com/kirbysayshi/tap-browser-color) +- [tap-json](https://github.com/gummesson/tap-json) +- [tap-min](https://github.com/derhuerst/tap-min) +- [tap-nyan](https://github.com/calvinmetcalf/tap-nyan) +- [tap-pessimist](https://www.npmjs.org/package/tap-pessimist) +- [tap-prettify](https://github.com/toolness/tap-prettify) +- [colortape](https://github.com/shuhei/colortape) +- [tap-xunit](https://github.com/aghassemi/tap-xunit) +- [tap-difflet](https://github.com/namuol/tap-difflet) +- [tape-dom](https://github.com/gritzko/tape-dom) +- [tap-diff](https://github.com/axross/tap-diff) +- [tap-notify](https://github.com/axross/tap-notify) +- [tap-summary](https://github.com/zoubin/tap-summary) +- [tap-markdown](https://github.com/Hypercubed/tap-markdown) +- [tap-html](https://github.com/gabrielcsapo/tap-html) +- [tap-react-browser](https://github.com/mcnuttandrew/tap-react-browser) +- [tap-junit](https://github.com/dhershman1/tap-junit) + +To use them, try `node test/index.js | tap-spec` or pipe it into one +of the modules of your choice! + +## uncaught exceptions + +By default, uncaught exceptions in your tests will not be intercepted, and will cause tape to crash. If you find this behavior undesirable, use [tape-catch](https://github.com/michaelrhodes/tape-catch) to report any exceptions as TAP errors. + +## other + +- CoffeeScript support with https://www.npmjs.com/package/coffeetape +- Promise support with https://www.npmjs.com/package/blue-tape or https://www.npmjs.com/package/tape-promise +- ES6 support with https://www.npmjs.com/package/babel-tape-runner or https://www.npmjs.com/package/buble-tape-runner +- Different test syntax with https://github.com/pguth/flip-tape (warning: mutates String.prototype) +- Electron test runner with https://github.com/tundrax/electron-tap +- Concurrency support with https://github.com/imsnif/mixed-tape + # methods The assertion methods in tape are heavily influenced or copied from the methods @@ -58,23 +142,44 @@ in [node-tap](https://github.com/isaacs/node-tap). var test = require('tape') ``` -## test(name, cb) +## test([name], [opts], cb) + +Create a new test with an optional `name` string and optional `opts` object. +`cb(t)` fires with the new test object `t` once all preceding tests have +finished. Tests execute serially. -Create a new test with an optional `name` string. `cb(t)` fires with the new -test object `t` once all preceeding tests have finished. Tests execute serially. +Available `opts` options are: +- opts.skip = true/false. See test.skip. +- opts.timeout = 500. Set a timeout for the test, after which it will fail. See test.timeoutAfter. +- opts.objectPrintDepth = 5. Configure max depth of expected / actual object printing. Environmental variable `NODE_TAPE_OBJECT_PRINT_DEPTH` can set the desired default depth for all tests; locally-set values will take precedence. +- opts.todo = true/false. Test will be allowed to fail. If you forget to `t.plan()` out how many assertions you are going to run and you don't call `t.end()` explicitly, your test will hang. +## test.skip([name], [opts], cb) + +Generate a new test that will be skipped over. + +## test.onFinish(fn) + +The onFinish hook will get invoked when ALL tape tests have finished +right before tape is about to print the test summary. + +## test.onFailure(fn) + +The onFailure hook will get invoked whenever any tape tests has failed. + ## t.plan(n) Declare that `n` assertions should be run. `t.end()` will be called automatically after the `n`th assertion. If there are any more assertions after the `n`th, or after `t.end()` is called, they will generate errors. -## t.end() +## t.end(err) -Declare the end of a test explicitly. +Declare the end of a test explicitly. If `err` is passed in `t.end` will assert +that it is falsey. ## t.fail(msg) @@ -84,19 +189,23 @@ Generate a failing assertion with a message `msg`. Generate a passing assertion with a message `msg`. +## t.timeoutAfter(ms) + +Automatically timeout the test after X ms. + ## t.skip(msg) - + Generate an assertion that will be skipped over. ## t.ok(value, msg) -Assert that `value` is truthy with an optional description message `msg`. +Assert that `value` is truthy with an optional description of the assertion `msg`. Aliases: `t.true()`, `t.assert()` ## t.notOk(value, msg) -Assert that `value` is falsy with an optional description message `msg`. +Assert that `value` is falsy with an optional description of the assertion `msg`. Aliases: `t.false()`, `t.notok()` @@ -107,74 +216,177 @@ description message. Aliases: `t.ifError()`, `t.ifErr()`, `t.iferror()` -## t.equal(a, b, msg) +## t.equal(actual, expected, msg) -Assert that `a === b` with an optional description `msg`. +Assert that `actual === expected` with an optional description of the assertion `msg`. Aliases: `t.equals()`, `t.isEqual()`, `t.is()`, `t.strictEqual()`, `t.strictEquals()` -## t.notEqual(a, b, msg) +## t.notEqual(actual, expected, msg) -Assert that `a !== b` with an optional description `msg`. +Assert that `actual !== expected` with an optional description of the assertion `msg`. Aliases: `t.notEquals()`, `t.notStrictEqual()`, `t.notStrictEquals()`, `t.isNotEqual()`, `t.isNot()`, `t.not()`, `t.doesNotEqual()`, `t.isInequal()` -## t.deepEqual(a, b, msg) +## t.deepEqual(actual, expected, msg) -Assert that `a` and `b` have the same structure and nested values using +Assert that `actual` and `expected` have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/substack/node-deep-equal) -with an optional description `msg`. +with strict comparisons (`===`) on leaf nodes and an optional description of the assertion `msg`. -Aliases: `t.deepEquals()`, `t.isEquivalent()`, `t.looseEqual()`, -`t.looseEquals()`, `t.same()` +Aliases: `t.deepEquals()`, `t.isEquivalent()`, `t.same()` -## t.notDeepEqual(a, b, msg) +## t.notDeepEqual(actual, expected, msg) -Assert that `a` and `b` do not have the same structure and nested values using +Assert that `actual` and `expected` do not have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/substack/node-deep-equal) -with an optional description `msg`. +with strict comparisons (`===`) on leaf nodes and an optional description of the assertion `msg`. -Aliases: `t.notEquivalent()`, `t.notDeeply()`, `t.notSame()`, +Aliases: `t.notDeepEquals`, `t.notEquivalent()`, `t.notDeeply()`, `t.notSame()`, `t.isNotDeepEqual()`, `t.isNotDeeply()`, `t.isNotEquivalent()`, `t.isInequivalent()` +## t.deepLooseEqual(actual, expected, msg) + +Assert that `actual` and `expected` have the same structure and nested values using +[node's deepEqual() algorithm](https://github.com/substack/node-deep-equal) +with loose comparisons (`==`) on leaf nodes and an optional description of the assertion `msg`. + +Aliases: `t.looseEqual()`, `t.looseEquals()` + +## t.notDeepLooseEqual(actual, expected, msg) + +Assert that `actual` and `expected` do not have the same structure and nested values using +[node's deepEqual() algorithm](https://github.com/substack/node-deep-equal) +with loose comparisons (`==`) on leaf nodes and an optional description of the assertion `msg`. + +Aliases: `t.notLooseEqual()`, `t.notLooseEquals()` + ## t.throws(fn, expected, msg) -Assert that the function call `fn()` throws an exception. +Assert that the function call `fn()` throws an exception. `expected`, if present, must be a `RegExp` or `Function`. The `RegExp` matches the string representation of the exception, as generated by `err.toString()`. The `Function` is the exception thrown (e.g. `Error`). `msg` is an optional description of the assertion. ## t.doesNotThrow(fn, expected, msg) -Assert that the function call `fn()` does not throw an exception. +Assert that the function call `fn()` does not throw an exception. `expected`, if present, limits what should not be thrown. For example, set `expected` to `/user/` to fail the test only if the string representation of the exception contains the word `user`. Any other exception would pass the test. If `expected` is omitted, any exception will fail the test. `msg` is an optional description of the assertion. -## t.test(name, cb) +## t.test(name, [opts], cb) Create a subtest with a new test handle `st` from `cb(st)` inside the current test `t`. `cb(st)` will only fire when `t` finishes. Additional tests queued up after `t` will not be run until all subtests finish. +You may pass the same options that [`test()`](#testname-opts-cb) accepts. + +## t.comment(message) + +Print a message without breaking the tap output. (Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.) + ## var htest = test.createHarness() Create a new test harness instance, which is a function like `test()`, but with a new pending stack and test state. -By default the TAP output goes to `process.stdout` or `console.log()` if the -environment doesn't have `process.stdout`. You can pipe the output to someplace -else if you `test.stream.pipe()` to a destination stream on the first tick. +By default the TAP output goes to `console.log()`. You can pipe the output to +someplace else if you `htest.createStream().pipe()` to a destination stream on +the first tick. -## test.only(name, cb) +## test.only([name], [opts], cb) -Like `test(name, cb)` except if you use `.only` this is the only test case +Like `test([name], [opts], cb)` except if you use `.only` this is the only test case that will run for the entire process, all other test cases using tape will be ignored +## var stream = test.createStream(opts) + +Create a stream of output, bypassing the default output stream that writes +messages to `console.log()`. By default `stream` will be a text stream of TAP +output, but you can get an object stream instead by setting `opts.objectMode` to +`true`. + +### tap stream reporter + +You can create your own custom test reporter using this `createStream()` api: + +``` js +var test = require('tape'); +var path = require('path'); + +test.createStream().pipe(process.stdout); + +process.argv.slice(2).forEach(function (file) { + require(path.resolve(file)); +}); +``` + +You could substitute `process.stdout` for whatever other output stream you want, +like a network connection or a file. + +Pass in test files to run as arguments: + +``` +$ node tap.js test/x.js test/y.js +TAP version 13 +# (anonymous) +not ok 1 should be equal + --- + operator: equal + expected: "boop" + actual: "beep" + ... +# (anonymous) +ok 2 should be equal +ok 3 (unnamed assert) +# wheee +ok 4 (unnamed assert) + +1..4 +# tests 4 +# pass 3 +# fail 1 +``` + +### object stream reporter + +Here's how you can render an object stream instead of TAP: + +``` js +var test = require('tape'); +var path = require('path'); + +test.createStream({ objectMode: true }).on('data', function (row) { + console.log(JSON.stringify(row)) +}); + +process.argv.slice(2).forEach(function (file) { + require(path.resolve(file)); +}); +``` + +The output for this runner is: + +``` +$ node object.js test/x.js test/y.js +{"type":"test","name":"(anonymous)","id":0} +{"id":0,"ok":false,"name":"should be equal","operator":"equal","actual":"beep","expected":"boop","error":{},"test":0,"type":"assert"} +{"type":"end","test":0} +{"type":"test","name":"(anonymous)","id":1} +{"id":0,"ok":true,"name":"should be equal","operator":"equal","actual":2,"expected":2,"test":1,"type":"assert"} +{"id":1,"ok":true,"name":"(unnamed assert)","operator":"ok","actual":true,"expected":true,"test":1,"type":"assert"} +{"type":"end","test":1} +{"type":"test","name":"wheee","id":2} +{"id":0,"ok":true,"name":"(unnamed assert)","operator":"ok","actual":true,"expected":true,"test":2,"type":"assert"} +{"type":"end","test":2} +``` + # install With [npm](https://npmjs.org) do: ``` -npm install tape +npm install tape --save-dev ``` # license diff --git a/test/add-subtest-async.js b/test/add-subtest-async.js new file mode 100644 index 00000000..efb89060 --- /dev/null +++ b/test/add-subtest-async.js @@ -0,0 +1,12 @@ +var test = require('../') + +test('parent', function (t) { + t.pass('parent'); + setTimeout(function () { + t.test('child', function (st) { + st.pass('child'); + st.end(); + }); + t.end(); + }, 100); +}) diff --git a/test/anonymous-fn.js b/test/anonymous-fn.js new file mode 100644 index 00000000..e05b967c --- /dev/null +++ b/test/anonymous-fn.js @@ -0,0 +1,49 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; +var testWrapper = require('./anonymous-fn/test-wrapper'); + +tap.test('inside anonymous functions', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var tc = function (rows) { + var body = stripFullStack(rows.toString('utf8')); + + // Handle stack trace variation in Node v0.8 + body = body.replace( + 'at Test.module.exports', + 'at Test.' + ); + + tt.same(body, [ + 'TAP version 13', + '# wrapped test failure', + 'not ok 1 fail', + ' ---', + ' operator: fail', + ' at: ($TEST/anonymous-fn.js:$LINE:$COL)', + ' stack: |-', + ' Error: fail', + ' [... stack stripped ...]', + ' at $TEST/anonymous-fn.js:$LINE:$COL', + ' at Test. ($TEST/anonymous-fn/test-wrapper.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + '', + '1..1', + '# tests 1', + '# pass 0', + '# fail 1' + ].join('\n') + '\n'); + }; + + test.createStream().pipe(concat(tc)); + + test('wrapped test failure', testWrapper(function (t) { + t.fail('fail'); + t.end(); + })); +}); diff --git a/test/anonymous-fn/test-wrapper.js b/test/anonymous-fn/test-wrapper.js new file mode 100644 index 00000000..9702c9f1 --- /dev/null +++ b/test/anonymous-fn/test-wrapper.js @@ -0,0 +1,16 @@ +// Example of wrapper function that would invoke tape +module.exports = function (testCase) { + return function(t) { + setUp(); + testCase(t); + tearDown(); + }; +} + +function setUp() { + // ... example ... +} + +function tearDown() { + // ... example ... +} diff --git a/test/array.js b/test/array.js index 2d498632..dc4a4a40 100644 --- a/test/array.js +++ b/test/array.js @@ -1,60 +1,53 @@ var falafel = require('falafel'); var tape = require('../'); var tap = require('tap'); +var concat = require('concat-stream'); tap.test('array test', function (tt) { tt.plan(1); - + var test = tape.createHarness(); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - tt.same(rs, [ + + test.createStream().pipe(concat(function (rows) { + tt.same(rows.toString('utf8'), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: true, name: 'should be equivalent' }, - 'tests 5', - 'pass 5', - 'ok' - ]); - }); - - test.createStream().pipe(tc); - + '# array', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'ok 5 should be equivalent', + '', + '1..5', + '# tests 5', + '# pass 5', + '', + '# ok' + ].join('\n') + '\n'); + })); + test('array', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/async_end.js b/test/async_end.js new file mode 100644 index 00000000..2e47e60c --- /dev/null +++ b/test/async_end.js @@ -0,0 +1,21 @@ +var test = require('../'); + +test('async end', function (t) { + setTimeout(function () { + t.assert(!t.ended, '!t.ended'); + t.end(); + }, 200); +}); + +test('async end with subtest', function (t) { + setTimeout(function () { + t.assert(!t.ended, '!t.ended'); + t.end(); + }, 200); + + t.test('subtest', function (g) { + g.assert(!t.ended, 'subtest !t.ended'); + g.end(); + }); +}); + diff --git a/test/bound.js b/test/bound.js new file mode 100644 index 00000000..58f26920 --- /dev/null +++ b/test/bound.js @@ -0,0 +1,10 @@ +var test = require('../'); + +test('bind works', function (t) { + t.plan(2); + var equal = t.equal; + var deepEqual = t.deepEqual; + equal(3, 3); + deepEqual([4], [4]); + t.end(); +}); diff --git a/test/child_ordering.js b/test/child_ordering.js index 03b88e20..12efafe9 100644 --- a/test/child_ordering.js +++ b/test/child_ordering.js @@ -20,10 +20,13 @@ var grandParentRan = false; var parentRan = false; var grandChildRan = false; test('grandparent', function(t) { + t.ok(!grandParentRan, 'grand parent ran twice'); grandParentRan = true; t.test('parent', function(t) { + t.ok(!parentRan, 'parent ran twice'); parentRan = true; t.test('grandchild', function(t) { + t.ok(!grandChildRan, 'grand child ran twice'); grandChildRan = true; t.pass('grand child ran'); t.end(); diff --git a/test/circular-things.js b/test/circular-things.js index 4ff4fece..4bda3e14 100644 --- a/test/circular-things.js +++ b/test/circular-things.js @@ -1,43 +1,44 @@ var tape = require('../'); var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; tap.test('circular test', function (assert) { var test = tape.createHarness({ exit : false }); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - // console.log("rs", rows) - - // console.log("deepEqual?") - - assert.same(rows, [ - "TAP version 13" - , "circular" - , { id: 1 - , ok: false - , name: " should be equal" - , operator: "equal" - , expected: "{}" - , actual: '{"circular":"[Circular]"}' - } - , "tests 1" - , "pass 0" - , "fail 1" - ]) - assert.end() - }) - - // tt.equal(10, 10) - // tt.end() + assert.plan(1); - test.createStream().pipe(tc); + test.createStream().pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# circular\n' + + 'not ok 1 should be equal\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: |-\n' + + ' {}\n' + + ' actual: |-\n' + + ' { circular: [Circular] }\n' + + ' at: Test. ($TEST/circular-things.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should be equal\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/circular-things.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + })); test("circular", function (t) { - t.plan(1) - var circular = {} - circular.circular = circular - t.equal(circular, {}) + t.plan(1); + var circular = {}; + circular.circular = circular; + t.equal(circular, {}); }) }) diff --git a/test/comment.js b/test/comment.js new file mode 100644 index 00000000..b3f9bcc3 --- /dev/null +++ b/test/comment.js @@ -0,0 +1,175 @@ +var concat = require('concat-stream'); +var tap = require('tap'); +var tape = require('../'); + +// Exploratory test to ascertain proper output when no t.comment() call +// is made. +tap.test('no comment', function (assert) { + assert.plan(1); + + var verify = function (output) { + assert.equal(output.toString('utf8'), [ + 'TAP version 13', + '# no comment', + '', + '1..0', + '# tests 0', + '# pass 0', + '', + '# ok', + '' + ].join('\n')); + }; + + var test = tape.createHarness(); + test.createStream().pipe(concat(verify)); + test('no comment', function (t) { + t.end(); + }); +}); + +// Exploratory test, can we call t.comment() passing nothing? +tap.test('missing argument', function (assert) { + assert.plan(1); + var test = tape.createHarness(); + test.createStream(); + test('missing argument', function (t) { + try { + t.comment(); + t.end(); + } catch (err) { + assert.equal(err.constructor, TypeError); + } finally { + assert.end(); + } + }); +}); + +// Exploratory test, can we call t.comment() passing nothing? +tap.test('null argument', function (assert) { + assert.plan(1); + var test = tape.createHarness(); + test.createStream(); + test('null argument', function (t) { + try { + t.comment(null); + t.end(); + } catch (err) { + assert.equal(err.constructor, TypeError); + } finally { + assert.end(); + } + }); +}); + + +// Exploratory test, how is whitespace treated? +tap.test('whitespace', function (assert) { + assert.plan(1); + + var verify = function (output) { + assert.equal(output.toString('utf8'), [ + 'TAP version 13', + '# whitespace', + '# ', + '# a', + '# a', + '# a', + '', + '1..0', + '# tests 0', + '# pass 0', + '', + '# ok', + '' + ].join('\n')); + }; + + var test = tape.createHarness(); + test.createStream().pipe(concat(verify)); + test('whitespace', function (t) { + t.comment(' '); + t.comment(' a'); + t.comment('a '); + t.comment(' a '); + t.end(); + }); +}); + +// Exploratory test, how about passing types other than strings? +tap.test('non-string types', function (assert) { + assert.plan(1); + + var verify = function (output) { + assert.equal(output.toString('utf8'), [ + 'TAP version 13', + '# non-string types', + '# true', + '# false', + '# 42', + '# 6.66', + '# [object Object]', + '# [object Object]', + '# [object Object]', + '# function ConstructorFunction() {}', + '', + '1..0', + '# tests 0', + '# pass 0', + '', + '# ok', + '' + ].join('\n')); + }; + + var test = tape.createHarness(); + test.createStream().pipe(concat(verify)); + test('non-string types', function (t) { + t.comment(true); + t.comment(false); + t.comment(42); + t.comment(6.66); + t.comment({}); + t.comment({"answer": 42}); + function ConstructorFunction() {} + t.comment(new ConstructorFunction()); + t.comment(ConstructorFunction); + t.end(); + }); +}); + +tap.test('multiline string', function (assert) { + assert.plan(1); + + var verify = function (output) { + assert.equal(output.toString('utf8'), [ + 'TAP version 13', + '# multiline strings', + '# a', + '# b', + '# c', + '# d', + '', + '1..0', + '# tests 0', + '# pass 0', + '', + '# ok', + '' + ].join('\n')); + }; + + var test = tape.createHarness(); + test.createStream().pipe(concat(verify)); + test('multiline strings', function (t) { + t.comment([ + 'a', + 'b', + ].join('\n')); + t.comment([ + 'c', + 'd', + ].join('\r\n')); + t.end(); + }); +}); diff --git a/test/common.js b/test/common.js new file mode 100644 index 00000000..f156693a --- /dev/null +++ b/test/common.js @@ -0,0 +1,63 @@ +var path = require('path'); +var yaml = require('js-yaml'); + +module.exports.getDiag = function (body) { + var yamlStart = body.indexOf(' ---'); + var yamlEnd = body.indexOf(' ...\n'); + var diag = body.slice(yamlStart, yamlEnd).split('\n').map(function (line) { + return line.slice(2); + }).join('\n'); + + // The stack trace and at variable will vary depending on where the code + // is run, so just strip it out. + var withStack = yaml.safeLoad(diag); + delete withStack.stack; + delete withStack.at; + return withStack; +} + +// There are three challenges associated with checking the stack traces included +// in errors: +// 1) The base checkout directory of tape might change. Because stack traces +// include absolute paths, the stack traces will change depending on the +// checkout path. We handle this by replacing the base test directory with a +// placeholder $TEST variable and the package root with a placehodler +// $TAPE variable. +// 2) Line positions within the file might change. We handle this by replacing +// line and column markers with placeholder $LINE and $COL "variables" +// a) node 0.8 does not provide nested eval line numbers, so we remove them +// 3) Stacks themselves change frequently with refactoring. We've even run into +// issues with node library refactorings "breaking" stack traces. Most of +// these changes are irrelevant to the tests themselves. To counter this, we +// strip out all stack frames that aren't directly under our test directory, +// and replace them with placeholders. +module.exports.stripFullStack = function (output) { + var stripped = ' [... stack stripped ...]'; + var withDuplicates = output.split('\n').map(function (line) { + var m = line.match(/[ ]{8}at .*\((.*)\)/); + + var stripChangingData = function (line) { + var withoutTestDir = line.replace(__dirname, '$TEST'); + var withoutPackageDir = withoutTestDir.replace(path.dirname(__dirname), '$TAPE'); + var withoutPathSep = withoutPackageDir.replace(new RegExp('\\' + path.sep, 'g'), '/'); + var withoutLineNumbers = withoutPathSep.replace(/:\d+:\d+/g, ':$LINE:$COL'); + var withoutNestedLineNumbers = withoutLineNumbers.replace(/, \:\$LINE:\$COL\)$/, ')'); + return withoutNestedLineNumbers; + } + + if (m) { + if (m[1].slice(0, __dirname.length) === __dirname) { + return stripChangingData(line); + } + return stripped; + } + return stripChangingData(line); + }) + + var deduped = withDuplicates.filter(function (line, ix) { + var hasPrior = line === stripped && withDuplicates[ix - 1] === stripped; + return !hasPrior; + }); + + return deduped.join('\n'); +} diff --git a/test/create_multiple_streams.js b/test/create_multiple_streams.js new file mode 100644 index 00000000..4b87d044 --- /dev/null +++ b/test/create_multiple_streams.js @@ -0,0 +1,31 @@ +var tape = require('../'); + +tape.test('createMultipleStreams', function(tt) { + tt.plan(2); + + var th = tape.createHarness(); + th.createStream() + th.createStream() + + var testOneComplete = false; + + th('test one', function (tht) { + tht.plan(1); + setTimeout( function() { + tht.pass(); + testOneComplete = true; + }, 100); + }); + + th('test two', function (tht) { + tht.ok(testOneComplete, 'test 1 completed before test 2'); + tht.end(); + }); + + th.onFinish(function() { + tt.equal(th._results.count, 2, "harness test ran"); + tt.equal(th._results.fail, 0, "harness test didn't fail"); + }); +}); + + diff --git a/test/deep-equal-failure.js b/test/deep-equal-failure.js new file mode 100644 index 00000000..48d0687c --- /dev/null +++ b/test/deep-equal-failure.js @@ -0,0 +1,191 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var tapParser = require('tap-parser'); +var common = require('./common'); + +var getDiag = common.getDiag; +var stripFullStack = common.stripFullStack; + +tap.test('deep equal failure', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# deep equal\n' + + 'not ok 1 should be equal\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: |-\n' + + ' { b: 2 }\n' + + ' actual: |-\n' + + ' { a: 1 }\n' + + ' at: Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should be equal\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'equal', + expected: '{ b: 2 }', + actual: '{ a: 1 }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should be equal', + diag: { + operator: 'equal', + expected: '{ b: 2 }', + actual: '{ a: 1 }' + } + }); + }); + + test("deep equal", function (t) { + t.plan(1); + t.equal({a: 1}, {b: 2}); + }); +}) + +tap.test('deep equal failure, depth 6, with option', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# deep equal\n' + + 'not ok 1 should be equal\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: |-\n' + + ' { a: { a1: { a2: { a3: { a4: { a5: 2 } } } } } }\n' + + ' actual: |-\n' + + ' { a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }\n' + + ' at: Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should be equal\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'equal', + expected: '{ a: { a1: { a2: { a3: { a4: { a5: 2 } } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should be equal', + diag: { + operator: 'equal', + expected: '{ a: { a1: { a2: { a3: { a4: { a5: 2 } } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }' + } + }); + }); + + test("deep equal", {objectPrintDepth: 6}, function (t) { + t.plan(1); + t.equal({ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }, { a: { a1: { a2: { a3: { a4: { a5: 2 } } } } } }); + }); +}) + +tap.test('deep equal failure, depth 6, without option', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# deep equal\n' + + 'not ok 1 should be equal\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: |-\n' + + ' { a: { a1: { a2: { a3: { a4: [Object] } } } } }\n' + + ' actual: |-\n' + + ' { a: { a1: { a2: { a3: { a4: [Object] } } } } }\n' + + ' at: Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should be equal\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'equal', + expected: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should be equal', + diag: { + operator: 'equal', + expected: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }' + } + }); + }); + + test("deep equal", function (t) { + t.plan(1); + t.equal({ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }, { a: { a1: { a2: { a3: { a4: { a5: 2 } } } } } }); + }); +}) diff --git a/test/deep.js b/test/deep.js new file mode 100644 index 00000000..909ebe10 --- /dev/null +++ b/test/deep.js @@ -0,0 +1,17 @@ +var test = require('../'); + +test('deep strict equal', function (t) { + t.notDeepEqual( + [ { a: '3' } ], + [ { a: 3 } ] + ); + t.end(); +}); + +test('deep loose equal', function (t) { + t.deepLooseEqual( + [ { a: '3' } ], + [ { a: 3 } ] + ); + t.end(); +}); diff --git a/test/default-messages.js b/test/default-messages.js new file mode 100644 index 00000000..dcfdbffc --- /dev/null +++ b/test/default-messages.js @@ -0,0 +1,31 @@ +var tap = require('tap'); +var path = require('path'); +var spawn = require('child_process').spawn; +var concat = require('concat-stream'); + +tap.test('default messages', function (t) { + t.plan(1); + + var ps = spawn(process.execPath, [path.join(__dirname, 'messages', 'defaults.js')]); + + ps.stdout.pipe(concat(function (rows) { + + t.same(rows.toString('utf8'), [ + 'TAP version 13', + '# default messages', + 'ok 1 should be truthy', + 'ok 2 should be falsy', + 'ok 3 should be equal', + 'ok 4 should not be equal', + 'ok 5 should be equivalent', + 'ok 6 should be equivalent', + 'ok 7 should be equivalent', + '', + '1..7', + '# tests 7', + '# pass 7', + '', + '# ok' + ].join('\n') + '\n\n'); + })); +}); diff --git a/test/double_end.js b/test/double_end.js new file mode 100644 index 00000000..a24f4507 --- /dev/null +++ b/test/double_end.js @@ -0,0 +1,60 @@ +var test = require('tap').test; +var path = require('path'); +var concat = require('concat-stream'); +var spawn = require('child_process').spawn; + +var stripFullStack = require('./common').stripFullStack; + +test(function (t) { + t.plan(2); + var ps = spawn(process.execPath, [path.join(__dirname, 'double_end', 'double.js')]); + ps.on('exit', function (code) { + t.equal(code, 1); + }); + ps.stdout.pipe(concat(function (body) { + // The implementation of node's timer library has changed over time. We + // need to reverse engineer the error we expect to see. + + // This code is unfortunately by necessity highly coupled to node + // versions, and may require tweaking with future versions of the timers + // library. + function doEnd() { throw new Error() }; + var to = setTimeout(doEnd, 5000); + clearTimeout(to); + to._onTimeout = doEnd; + + var stackExpected; + var atExpected; + try { + to._onTimeout(); + } + catch (e) { + stackExpected = stripFullStack(e.stack).split('\n')[1]; + stackExpected = stackExpected.replace('double_end.js', 'double_end/double.js'); + stackExpected = stackExpected.trim(); + atExpected = stackExpected.replace(/^at\s+/, 'at: '); + } + + var stripped = stripFullStack(body.toString('utf8')); + t.equal(stripped, [ + 'TAP version 13', + '# double end', + 'ok 1 should be equal', + 'not ok 2 .end() called twice', + ' ---', + ' operator: fail', + ' ' + atExpected, + ' stack: |-', + ' Error: .end() called twice', + ' [... stack stripped ...]', + ' ' + stackExpected, + ' [... stack stripped ...]', + ' ...', + '', + '1..2', + '# tests 2', + '# pass 1', + '# fail 1', + ].join('\n') + '\n\n'); + })); +}); diff --git a/test/double_end/double.js b/test/double_end/double.js new file mode 100644 index 00000000..43929e51 --- /dev/null +++ b/test/double_end/double.js @@ -0,0 +1,11 @@ +var test = require('../../'); + +test('double end', function (t) { + function doEnd() { + t.end(); + } + + t.equal(1 + 1, 2); + t.end(); + setTimeout(doEnd, 5); +}); diff --git a/test/end-as-callback.js b/test/end-as-callback.js new file mode 100644 index 00000000..f8791c0a --- /dev/null +++ b/test/end-as-callback.js @@ -0,0 +1,88 @@ +var tap = require("tap"); +var forEach = require("for-each"); +var tape = require("../"); +var concat = require('concat-stream'); + +tap.test("tape assert.end as callback", function (tt) { + var test = tape.createHarness({ exit: false }) + + test.createStream().pipe(concat(function (rows) { + tt.equal(rows.toString('utf8'), [ + 'TAP version 13', + '# do a task and write', + 'ok 1 null', + 'ok 2 should be equal', + '# do a task and write fail', + 'ok 3 null', + 'ok 4 should be equal', + 'not ok 5 Error: fail', + getStackTrace(rows), // tap error stack + '', + '1..5', + '# tests 5', + '# pass 4', + '# fail 1' + ].join('\n') + '\n'); + tt.end() + })); + + test("do a task and write", function (assert) { + fakeAsyncTask("foo", function (err, value) { + assert.ifError(err) + assert.equal(value, "taskfoo") + + fakeAsyncWrite("bar", assert.end) + }) + }) + + test("do a task and write fail", function (assert) { + fakeAsyncTask("bar", function (err, value) { + assert.ifError(err) + assert.equal(value, "taskbar") + + fakeAsyncWriteFail("baz", assert.end) + }) + }) +}) + +function fakeAsyncTask(name, cb) { + cb(null, "task" + name) +} + +function fakeAsyncWrite(name, cb) { + cb(null) +} + +function fakeAsyncWriteFail(name, cb) { + cb(new Error("fail")) +} + +/** + * extract the stack trace for the failed test. + * this will change dependent on the environment + * so no point hard-coding it in the test assertion + * see: https://git.io/v6hGG for example + * @param String rows - the tap output lines + * @returns String stacktrace - just the error stack part + */ +function getStackTrace(rows) { + var stacktrace = ' ---\n'; + var extract = false; + forEach(rows.toString('utf8').split('\n'), function (row) { + if (!extract) { + if (row.indexOf('---') > -1) { // start of stack trace + extract = true; + } + } else { + if (row.indexOf('...') > -1) { // end of stack trace + extract = false; + stacktrace += ' ...'; + } else { + stacktrace += row + '\n'; + } + + } + }); + // console.log(stacktrace); + return stacktrace; +} diff --git a/test/exit.js b/test/exit.js index 7f7c5d01..283301fc 100644 --- a/test/exit.js +++ b/test/exit.js @@ -1,36 +1,36 @@ var tap = require('tap'); +var path = require('path'); var spawn = require('child_process').spawn; +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; tap.test('exit ok', function (t) { t.plan(2); - - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - t.same(rs, [ + + var tc = function (rows) { + t.same(rows.toString('utf8'), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: true, name: 'should be equivalent' }, - 'tests 5', - 'pass 5', - 'ok' - ]); - }); - - var ps = spawn(process.execPath, [ __dirname + '/exit/ok.js' ]); - ps.stdout.pipe(tc); + '# array', + '# hi', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'ok 5 should be equivalent', + '', + '1..5', + '# tests 5', + '# pass 5', + '', + '# ok', + '', // yes, these double-blank-lines at the end are required. + '' // if you can figure out how to remove them, please do! + ].join('\n')); + } + + var ps = spawn(process.execPath, [path.join(__dirname, 'exit', 'ok.js')]); + ps.stdout.pipe(concat(tc)); ps.on('exit', function (code) { t.equal(code, 0); }); @@ -38,34 +38,40 @@ tap.test('exit ok', function (t) { tap.test('exit fail', function (t) { t.plan(2); - - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - t.same(rs, [ + + var tc = function (rows) { + t.same(stripFullStack(rows.toString('utf8')), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: false, name: 'should be equivalent' }, - 'tests 5', - 'pass 4', - 'fail 1' - ]); - }); - - var ps = spawn(process.execPath, [ __dirname + '/exit/fail.js' ]); - ps.stdout.pipe(tc); + '# array', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'not ok 5 should be equivalent', + ' ---', + ' operator: deepEqual', + ' expected: [ [ 1, 2, [ 3, 4444 ] ], [ 5, 6 ] ]', + ' actual: [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ]', + ' at: ($TEST/exit/fail.js:$LINE:$COL)', + ' stack: |-', + ' Error: should be equivalent', + ' [... stack stripped ...]', + ' at $TEST/exit/fail.js:$LINE:$COL', + ' at eval (eval at ($TEST/exit/fail.js:$LINE:$COL))', + ' at eval (eval at ($TEST/exit/fail.js:$LINE:$COL))', + ' at Test. ($TEST/exit/fail.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + '', + '1..5', + '# tests 5', + '# pass 4', + '# fail 1' + ].join('\n') + '\n\n'); + }; + + var ps = spawn(process.execPath, [path.join(__dirname, 'exit', 'fail.js')]); + ps.stdout.pipe(concat(tc)); ps.on('exit', function (code) { t.notEqual(code, 0); }); @@ -73,35 +79,36 @@ tap.test('exit fail', function (t) { tap.test('too few exit', function (t) { t.plan(2); - - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - t.same(rs, [ + + var tc = function (rows) { + t.same(stripFullStack(rows.toString('utf8')), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: true, name: 'should be equivalent' }, - { id: 6, ok: false, name: 'plan != count' }, - 'tests 6', - 'pass 5', - 'fail 1' - ]); - }); - - var ps = spawn(process.execPath, [ __dirname + '/exit/too_few.js' ]); - ps.stdout.pipe(tc); + '# array', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'ok 5 should be equivalent', + 'not ok 6 plan != count', + ' ---', + ' operator: fail', + ' expected: 6', + ' actual: 5', + ' at: process. ($TAPE/index.js:$LINE:$COL)', + ' stack: |-', + ' Error: plan != count', + ' [... stack stripped ...]', + ' ...', + '', + '1..6', + '# tests 6', + '# pass 5', + '# fail 1' + ].join('\n') + '\n\n'); + }; + + var ps = spawn(process.execPath, [path.join(__dirname, '/exit/too_few.js')]); + ps.stdout.pipe(concat(tc)); ps.on('exit', function (code) { t.notEqual(code, 0); }); @@ -109,33 +116,34 @@ tap.test('too few exit', function (t) { tap.test('more planned in a second test', function (t) { t.plan(2); - - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - t.same(rs, [ + + var tc = function (rows) { + t.same(stripFullStack(rows.toString('utf8')), [ 'TAP version 13', - 'first', - { id: 1, ok: true, name: '(unnamed assert)' }, - 'second', - { id: 2, ok: true, name: '(unnamed assert)' }, - { id: 3, ok: false, name: 'plan != count' }, - 'tests 3', - 'pass 2', - 'fail 1' - ]); - }); - - var ps = spawn(process.execPath, [ __dirname + '/exit/second.js' ]); - ps.stdout.pipe(tc); + '# first', + 'ok 1 should be truthy', + '# second', + 'ok 2 should be truthy', + 'not ok 3 plan != count', + ' ---', + ' operator: fail', + ' expected: 2', + ' actual: 1', + ' at: process. ($TAPE/index.js:$LINE:$COL)', + ' stack: |-', + ' Error: plan != count', + ' [... stack stripped ...]', + ' ...', + '', + '1..3', + '# tests 3', + '# pass 2', + '# fail 1' + ].join('\n') + '\n\n'); + }; + + var ps = spawn(process.execPath, [path.join(__dirname, '/exit/second.js')]); + ps.stdout.pipe(concat(tc)); ps.on('exit', function (code) { t.notEqual(code, 0); }); diff --git a/test/exit/fail.js b/test/exit/fail.js index d7fd3ce7..07a65ca3 100644 --- a/test/exit/fail.js +++ b/test/exit/fail.js @@ -3,26 +3,26 @@ var falafel = require('falafel'); test('array', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/exit/ok.js b/test/exit/ok.js index a02c7b69..6d405c7c 100644 --- a/test/exit/ok.js +++ b/test/exit/ok.js @@ -2,27 +2,28 @@ var falafel = require('falafel'); var test = require('../../'); test('array', function (t) { + t.comment('hi'); t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/exit/too_few.js b/test/exit/too_few.js index 8e60ce5b..68ba71db 100644 --- a/test/exit/too_few.js +++ b/test/exit/too_few.js @@ -3,26 +3,26 @@ var test = require('../../'); test('array', function (t) { t.plan(6); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/exposed-harness.js b/test/exposed-harness.js new file mode 100644 index 00000000..e1795eb0 --- /dev/null +++ b/test/exposed-harness.js @@ -0,0 +1,12 @@ +var tape = require('../'); +var tap = require('tap'); + +tap.test('main harness object is exposed', function (assert) { + + assert.equal(typeof tape.getHarness, 'function', 'tape.getHarness is a function') + + assert.equal(tape.getHarness()._results.pass, 0) + + assert.end() + +}) diff --git a/test/fail.js b/test/fail.js index d56045ad..133359ca 100644 --- a/test/fail.js +++ b/test/fail.js @@ -1,60 +1,70 @@ var falafel = require('falafel'); var tape = require('../'); var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; tap.test('array test', function (tt) { tt.plan(1); - + var test = tape.createHarness({ exit : false }); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - tt.same(rs, [ + var tc = function (rows) { + tt.same(stripFullStack(rows.toString('utf8')), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: false, name: 'should be equivalent' }, - 'tests 5', - 'pass 4', - 'fail 1' - ]); - }); - - test.createStream().pipe(tc); - + '# array', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'not ok 5 should be equivalent', + ' ---', + ' operator: deepEqual', + ' expected: [ [ 1, 2, [ 3, 4444 ] ], [ 5, 6 ] ]', + ' actual: [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ]', + ' at: ($TEST/fail.js:$LINE:$COL)', + ' stack: |-', + ' Error: should be equivalent', + ' [... stack stripped ...]', + ' at $TEST/fail.js:$LINE:$COL', + ' at eval (eval at ($TEST/fail.js:$LINE:$COL))', + ' at eval (eval at ($TEST/fail.js:$LINE:$COL))', + ' at Test. ($TEST/fail.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + '', + '1..5', + '# tests 5', + '# pass 4', + '# fail 1', + '' + ].join('\n')); + }; + + test.createStream().pipe(concat(tc)); + test('array', function (t) { t.plan(5); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/has spaces.js b/test/has spaces.js new file mode 100644 index 00000000..19a2b9e3 --- /dev/null +++ b/test/has spaces.js @@ -0,0 +1,40 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('array test', function (tt) { + tt.plan(1); + + var test = tape.createHarness({ exit : false }); + var tc = function (rows) { + tt.same(stripFullStack(rows.toString('utf8')), [ + 'TAP version 13', + '# fail', + 'not ok 1 this should fail', + ' ---', + ' operator: fail', + ' at: Test. ($TEST/has spaces.js:$LINE:$COL)', + ' stack: |-', + ' Error: this should fail', + ' [... stack stripped ...]', + ' at Test. ($TEST/has spaces.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + '', + '1..1', + '# tests 1', + '# pass 0', + '# fail 1', + '' + ].join('\n')); + }; + + test.createStream().pipe(concat(tc)); + + test('fail', function (t) { + t.fail('this should fail'); + t.end(); + }); +}); diff --git a/test/max_listeners.js b/test/max_listeners.js index 5edfb153..e807cdbf 100644 --- a/test/max_listeners.js +++ b/test/max_listeners.js @@ -1,5 +1,8 @@ var spawn = require('child_process').spawn; -var ps = spawn(process.execPath, [ __dirname + '/max_listeners/source.js' ]); +var path = require('path'); + +var ps = spawn(process.execPath, [path.join(__dirname, 'max_listeners', 'source.js')]); + ps.stdout.pipe(process.stdout, { end : false }); ps.stderr.on('data', function (buf) { diff --git a/test/max_listeners/source.js b/test/max_listeners/source.js index 839a3275..f78782fc 100644 --- a/test/max_listeners/source.js +++ b/test/max_listeners/source.js @@ -1,5 +1,5 @@ var test = require('../../'); for (var i = 0; i < 11; i ++) { - test(function (t) { t.end() }); + test(function (t) { t.ok(true, 'true is truthy'); t.end() }); } diff --git a/test/messages/defaults.js b/test/messages/defaults.js new file mode 100644 index 00000000..836a34aa --- /dev/null +++ b/test/messages/defaults.js @@ -0,0 +1,12 @@ +var test = require('../../'); + +test('default messages', function (t) { + t.plan(7); + t.ok(true); + t.notOk(false); + t.equal(true, true); + t.notEqual(true, false); + t.deepEqual(true, true); + t.deepLooseEqual(true, true); + t.notDeepLooseEqual(true, false); +}); diff --git a/test/nested-async-plan-noend.js b/test/nested-async-plan-noend.js new file mode 100644 index 00000000..69f43b9d --- /dev/null +++ b/test/nested-async-plan-noend.js @@ -0,0 +1,36 @@ +var test = require('../'); + +test('Harness async test support', function(t) { + t.plan(3); + + t.ok(true, 'sync child A'); + + t.test('sync child B', function(tt) { + tt.plan(2); + + setTimeout(function(){ + tt.test('async grandchild A', function(ttt) { + ttt.plan(1); + ttt.ok(true); + }); + }, 50); + + setTimeout(function() { + tt.test('async grandchild B', function(ttt) { + ttt.plan(1); + ttt.ok(true); + }); + }, 100); + }); + + setTimeout(function() { + t.test('async child', function(tt) { + tt.plan(2); + tt.ok(true, 'sync grandchild in async child A'); + tt.test('sync grandchild in async child B', function(ttt) { + ttt.plan(1); + ttt.ok(true); + }); + }); + }, 200); +}); diff --git a/test/nested-sync-noplan-noend.js b/test/nested-sync-noplan-noend.js new file mode 100644 index 00000000..aeb920d1 --- /dev/null +++ b/test/nested-sync-noplan-noend.js @@ -0,0 +1,51 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var stripFullStack = require('./common').stripFullStack; + +tap.test('nested sync test without plan or end', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var tc = function (rows) { + tt.same(stripFullStack(rows.toString('utf8')), [ + 'TAP version 13', + '# nested without plan or end', + 'not ok 1 test timed out after 100ms', + ' ---', + ' operator: fail', + ' stack: |-', + ' Error: test timed out after 100ms', + ' [... stack stripped ...]', + ' ...', + '# first', + 'ok 2 should be truthy', + '# second', + 'ok 3 should be truthy', + '', + '1..3', + '# tests 3', + '# pass 2', + '# fail 1', + ].join('\n') + '\n'); + }; + + test.createStream().pipe(concat(tc)); + + test('nested without plan or end', function(t) { + t.test('first', function(q) { + setTimeout(function first() { + q.ok(true); + q.end() + }, 10); + }); + t.test('second', function(q) { + setTimeout(function second() { + q.ok(true); + q.end() + }, 10); + }); + + t.timeoutAfter(100); + }); +}); diff --git a/test/nested.js b/test/nested.js index 6af123bc..46822594 100644 --- a/test/nested.js +++ b/test/nested.js @@ -1,74 +1,68 @@ var falafel = require('falafel'); var tape = require('../'); var tap = require('tap'); +var concat = require('concat-stream'); tap.test('array test', function (tt) { tt.plan(1); - + var test = tape.createHarness(); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - tt.same(rs, [ + var tc = function (rows) { + tt.same(rows.toString('utf8'), [ 'TAP version 13', - 'nested array test', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: true, name: 'should be equivalent' }, - 'inside test', - { id: 6, ok: true, name: '(unnamed assert)' }, - { id: 7, ok: true, name: '(unnamed assert)' }, - 'another', - { id: 8, ok: true, name: '(unnamed assert)' }, - 'tests 8', - 'pass 8', - 'ok' - ]); - }); - - test.createStream().pipe(tc); - + '# nested array test', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'ok 5 should be equivalent', + '# inside test', + 'ok 6 should be truthy', + 'ok 7 should be truthy', + '# another', + 'ok 8 should be truthy', + '', + '1..8', + '# tests 8', + '# pass 8', + '', + '# ok' + ].join('\n') + '\n'); + }; + + test.createStream().pipe(concat(tc)); + test('nested array test', function (t) { - t.plan(5); - + t.plan(6); + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + t.test('inside test', function (q) { q.plan(2); q.ok(true); - + setTimeout(function () { q.ok(true); }, 100); }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/nested2.js b/test/nested2.js new file mode 100644 index 00000000..58ae8f3d --- /dev/null +++ b/test/nested2.js @@ -0,0 +1,19 @@ +var test = require('../'); + +test(function(t) { + var i = 0 + t.test('setup', function(t) { + process.nextTick(function() { + t.equal(i, 0, 'called once') + i++ + t.end() + }) + }) + + + t.test('teardown', function(t) { + t.end() + }) + + t.end() +}) diff --git a/test/nested_test_ordering.js b/test/nested_test_ordering.js new file mode 100644 index 00000000..5abd4c3f --- /dev/null +++ b/test/nested_test_ordering.js @@ -0,0 +1,98 @@ + +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('plan vs end: plan', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + test.createStream().pipe(concat(function (rows) { + tt.same(rows.toString('utf8'), [ + 'TAP version 13', + '# first', + 'ok 1 first test', + 'ok 2 t not ended', + 'ok 3 t has progeny', + '# second', + 'ok 4 second test', + '# third', + 'ok 5 third test', + '', + '1..5', + '# tests 5', + '# pass 5', + '', + '# ok' + ].join('\n') + '\n'); + })); + + test('first', function (t) { + t.plan(4); + setTimeout(function () { + t.ok(1, 'first test'); + t.ok(!t.ended, 't not ended'); + t.ok(t._progeny.length, 't has progeny'); + }, 200); + + t.test('second', function (t) { + t.plan(1); + t.ok(1, 'second test'); + }); + }); + + test('third', function (t) { + t.plan(1); + setTimeout(function () { + t.ok(1, 'third test'); + }, 100); + }); +}); + +tap.test('plan vs end: end', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + test.createStream().pipe(concat(function (rows) { + tt.same(rows.toString('utf8'), [ + 'TAP version 13', + '# first', + 'ok 1 first test', + 'ok 2 t not ended', + 'ok 3 t has progeny', + '# second', + 'ok 4 second test', + '# third', + 'ok 5 third test', + '', + '1..5', + '# tests 5', + '# pass 5', + '', + '# ok' + ].join('\n') + '\n'); + })); + + test('first', function (t) { + setTimeout(function () { + t.ok(1, 'first test'); + t.ok(!t.ended, 't not ended'); + t.ok(t._progeny.length, 't has progeny'); + t.end(); + }, 200); + + t.test('second', function (t) { + t.ok(1, 'second test'); + t.end(); + }); + }); + + test('third', function (t) { + setTimeout(function () { + t.ok(1, 'third test'); + t.end(); + }, 100); + }); +}); diff --git a/test/no_callback.js b/test/no_callback.js new file mode 100644 index 00000000..760ff26c --- /dev/null +++ b/test/no_callback.js @@ -0,0 +1,3 @@ +var test = require('../'); + +test('No callback.'); diff --git a/test/not-deep-equal-failure.js b/test/not-deep-equal-failure.js new file mode 100644 index 00000000..efeed5b7 --- /dev/null +++ b/test/not-deep-equal-failure.js @@ -0,0 +1,191 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var tapParser = require('tap-parser'); +var common = require('./common'); + +var getDiag = common.getDiag; +var stripFullStack = common.stripFullStack; + +tap.test('deep equal failure', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# not deep equal\n' + + 'not ok 1 should not be equivalent\n' + + ' ---\n' + + ' operator: notDeepEqual\n' + + ' expected: |-\n' + + ' { b: 2 }\n' + + ' actual: |-\n' + + ' { b: 2 }\n' + + ' at: Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should not be equivalent\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'notDeepEqual', + expected: '{ b: 2 }', + actual: '{ b: 2 }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should not be equivalent', + diag: { + operator: 'notDeepEqual', + expected: '{ b: 2 }', + actual: '{ b: 2 }' + } + }); + }); + + test("not deep equal", function (t) { + t.plan(1); + t.notDeepEqual({b: 2}, {b: 2}); + }); +}) + +tap.test('not deep equal failure, depth 6, with option', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# not deep equal\n' + + 'not ok 1 should not be equivalent\n' + + ' ---\n' + + ' operator: notDeepEqual\n' + + ' expected: |-\n' + + ' { a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }\n' + + ' actual: |-\n' + + ' { a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }\n' + + ' at: Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should not be equivalent\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'notDeepEqual', + expected: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should not be equivalent', + diag: { + operator: 'notDeepEqual', + expected: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }' + } + }); + }); + + test("not deep equal", {objectPrintDepth: 6}, function (t) { + t.plan(1); + t.notDeepEqual({ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }, { a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }); + }); +}) + +tap.test('not deep equal failure, depth 6, without option', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# not deep equal\n' + + 'not ok 1 should not be equivalent\n' + + ' ---\n' + + ' operator: notDeepEqual\n' + + ' expected: |-\n' + + ' { a: { a1: { a2: { a3: { a4: [Object] } } } } }\n' + + ' actual: |-\n' + + ' { a: { a1: { a2: { a3: { a4: [Object] } } } } }\n' + + ' at: Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should not be equivalent\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/not-deep-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'notDeepEqual', + expected: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should not be equivalent', + diag: { + operator: 'notDeepEqual', + expected: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }', + actual: '{ a: { a1: { a2: { a3: { a4: [Object] } } } } }' + } + }); + }); + + test("not deep equal", function (t) { + t.plan(1); + t.notDeepEqual({ a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }, { a: { a1: { a2: { a3: { a4: { a5: 1 } } } } } }); + }); +}) diff --git a/test/not-equal-failure.js b/test/not-equal-failure.js new file mode 100644 index 00000000..763e363f --- /dev/null +++ b/test/not-equal-failure.js @@ -0,0 +1,67 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var tapParser = require('tap-parser'); +var common = require('./common'); + +var getDiag = common.getDiag; +var stripFullStack = common.stripFullStack; + +tap.test('not equal failure', function (assert) { + var test = tape.createHarness({ exit : false }); + var stream = test.createStream(); + var parser = tapParser(); + assert.plan(3); + + stream.pipe(parser); + stream.pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# not equal\n' + + 'not ok 1 should not be equal\n' + + ' ---\n' + + ' operator: notEqual\n' + + ' expected: 2\n' + + ' actual: 2\n' + + ' at: Test. ($TEST/not-equal-failure.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should not be equal\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/not-equal-failure.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + assert.deepEqual(getDiag(body), { + operator: 'notEqual', + expected: '2', + actual: '2' + }); + })); + + parser.once('assert', function (data) { + delete data.diag.stack; + delete data.diag.at; + assert.deepEqual(data, { + ok: false, + id: 1, + name: 'should not be equal', + diag: { + operator: 'notEqual', + expected: '2', + actual: '2' + } + }); + }); + + test("not equal", function (t) { + t.plan(1); + t.notEqual(2, 2); + }); +}) diff --git a/test/onFailure.js b/test/onFailure.js new file mode 100644 index 00000000..e8efdbdb --- /dev/null +++ b/test/onFailure.js @@ -0,0 +1,21 @@ +var tap = require("tap"); +var tape = require("../").createHarness(); + +//Because this test passing depends on a failure, +//we must direct the failing output of the inner test +var noop = function(){} +var mockSink = {on:noop, removeListener:noop, emit:noop, end:noop} +tape.createStream().pipe(mockSink); + +tap.test("on failure", { timeout: 1000 }, function(tt) { + tt.plan(1); + + tape("dummy test", function(t) { + t.fail(); + t.end(); + }); + + tape.onFailure(function() { + tt.pass("tape ended"); + }); +}); diff --git a/test/onFinish.js b/test/onFinish.js new file mode 100644 index 00000000..5b77fae2 --- /dev/null +++ b/test/onFinish.js @@ -0,0 +1,12 @@ +var tap = require("tap"); +var tape = require("../"); + +tap.test("on finish", {timeout: 1000}, function (tt) { + tt.plan(1); + tape.onFinish(function() { + tt.pass('tape ended'); + }); + tape('dummy test', function(t) { + t.end(); + }); +}); diff --git a/test/only-twice.js b/test/only-twice.js new file mode 100644 index 00000000..488e49ba --- /dev/null +++ b/test/only-twice.js @@ -0,0 +1,20 @@ +var tape = require('../'); +var tap = require('tap'); + +tap.test('only twice error', function (assert) { + var test = tape.createHarness({ exit : false }); + + test.only("first only", function (t) { + t.end() + }); + + assert.throws(function() { + test.only('second only', function(t) { + t.end(); + }); + }, { + name: 'Error', + message: 'there can only be one only test' + }); + assert.end(); +}); diff --git a/test/only.js b/test/only.js index 9e6bc26f..f68c450b 100644 --- a/test/only.js +++ b/test/only.js @@ -1,37 +1,29 @@ var tap = require('tap'); var tape = require('../'); +var concat = require('concat-stream'); tap.test('tape only test', function (tt) { var test = tape.createHarness({ exit: false }); - var tc = tap.createConsumer(); var ran = []; - var rows = [] - tc.on('data', function (r) { rows.push(r) }) - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id: r.id, ok: r.ok, name: r.name.trim() }; - } - else { - return r; - } - }) - - tt.deepEqual(rs, [ + var tc = function (rows) { + tt.deepEqual(rows.toString('utf8'), [ 'TAP version 13', - 'run success', - { id: 1, ok: true, name: 'assert name'}, - 'tests 1', - 'pass 1', - 'ok' - ]) + '# run success', + 'ok 1 assert name', + '', + '1..1', + '# tests 1', + '# pass 1', + '', + '# ok' + ].join('\n') + '\n'); tt.deepEqual(ran, [ 3 ]); tt.end() - }) + }; - test.createStream().pipe(tc) + test.createStream().pipe(concat(tc)); test("never run fail", function (t) { ran.push(1); diff --git a/test/only2.js b/test/only2.js new file mode 100644 index 00000000..fcf4f439 --- /dev/null +++ b/test/only2.js @@ -0,0 +1,9 @@ +var test = require('../'); + +test('only2 test 1', function (t) { + t.end(); +}); + +test.only('only2 test 2', function (t) { + t.end(); +}); diff --git a/test/only3.js b/test/only3.js new file mode 100644 index 00000000..b192a4e0 --- /dev/null +++ b/test/only3.js @@ -0,0 +1,15 @@ +var test = require('../'); + +test('only3 test 1', function (t) { + t.fail('not 1'); + t.end(); +}); + +test.only('only3 test 2', function (t) { + t.end(); +}); + +test('only3 test 3', function (t) { + t.fail('not 3'); + t.end(); +}); diff --git a/test/only4.js b/test/only4.js new file mode 100644 index 00000000..d570b5bc --- /dev/null +++ b/test/only4.js @@ -0,0 +1,10 @@ +var test = require('../'); + +test('only4 duplicate test name', function (t) { + t.fail('not 1'); + t.end(); +}); + +test.only('only4 duplicate test name', function (t) { + t.end(); +}); diff --git a/test/only5.js b/test/only5.js new file mode 100644 index 00000000..0e158872 --- /dev/null +++ b/test/only5.js @@ -0,0 +1,10 @@ +var test = require('../'); + +test.only('only5 duplicate test name', function (t) { + t.end(); +}); + +test('only5 duplicate test name', function (t) { + t.fail('not 2'); + t.end(); +}); diff --git a/test/require.js b/test/require.js new file mode 100644 index 00000000..5e39e170 --- /dev/null +++ b/test/require.js @@ -0,0 +1,69 @@ +var tap = require('tap'); +var spawn = require('child_process').spawn; +var concat = require('concat-stream'); + +tap.test('requiring a single module', function (t) { + t.plan(2); + + var tc = function (rows) { + t.same(rows.toString('utf8'), [ + 'TAP version 13', + '# module-a', + 'ok 1 loaded module a', + '# test-a', + 'ok 2 module-a loaded in same context', + 'ok 3 test ran after module-a was loaded', + '', + '1..3', + '# tests 3', + '# pass 3', + '', + '# ok' + ].join('\n') + '\n\n'); + }; + + var ps = tape('-r ./require/a require/test-a.js'); + ps.stdout.pipe(concat(tc)); + ps.on('exit', function (code) { + t.equal(code, 0); + }); +}); + +tap.test('requiring multiple modules', function (t) { + t.plan(2); + + var tc = function (rows) { + t.same(rows.toString('utf8'), [ + 'TAP version 13', + '# module-a', + 'ok 1 loaded module a', + '# module-b', + 'ok 2 loaded module b', + '# test-a', + 'ok 3 module-a loaded in same context', + 'ok 4 test ran after module-a was loaded', + '# test-b', + 'ok 5 module-b loaded in same context', + 'ok 6 test ran after module-b was loaded', + '', + '1..6', + '# tests 6', + '# pass 6', + '', + '# ok' + ].join('\n') + '\n\n'); + }; + + var ps = tape('-r ./require/a -r ./require/b require/test-a.js require/test-b.js'); + ps.stdout.pipe(concat(tc)); + ps.on('exit', function (code) { + t.equal(code, 0); + }); +}); + +function tape(args) { + var proc = require('child_process') + var bin = __dirname + '/../bin/tape' + + return proc.spawn('node', [bin].concat(args.split(' ')), { cwd: __dirname }) +} diff --git a/test/require/a.js b/test/require/a.js new file mode 100644 index 00000000..097eb883 --- /dev/null +++ b/test/require/a.js @@ -0,0 +1,8 @@ +var tape = require('../..'); + +tape.test('module-a', function(t) { + t.plan(1) + t.pass('loaded module a') +}) + +global.module_a = true diff --git a/test/require/b.js b/test/require/b.js new file mode 100644 index 00000000..2f10a0ec --- /dev/null +++ b/test/require/b.js @@ -0,0 +1,8 @@ +var tape = require('../..'); + +tape.test('module-b', function(t) { + t.plan(1) + t.pass('loaded module b') +}) + +global.module_b = true diff --git a/test/require/test-a.js b/test/require/test-a.js new file mode 100644 index 00000000..769e40cd --- /dev/null +++ b/test/require/test-a.js @@ -0,0 +1,7 @@ +var tape = require('../..'); + +tape.test('test-a', function(t) { + t.ok(global.module_a, 'module-a loaded in same context') + t.pass('test ran after module-a was loaded') + t.end() +}) diff --git a/test/require/test-b.js b/test/require/test-b.js new file mode 100644 index 00000000..4eb3b681 --- /dev/null +++ b/test/require/test-b.js @@ -0,0 +1,7 @@ +var tape = require('../..'); + +tape.test('test-b', function(t) { + t.ok(global.module_b, 'module-b loaded in same context') + t.pass('test ran after module-b was loaded') + t.end() +}) diff --git a/test/skip.js b/test/skip.js index 13dd9cfc..c3bf8d9e 100644 --- a/test/skip.js +++ b/test/skip.js @@ -1,24 +1,47 @@ var test = require('../'); var ran = 0; -test('do not skip this', { skip: false }, function(t) { - t.pass('this should run'); - ran ++; - t.end(); +var concat = require('concat-stream'); +var tap = require('tap'); + +tap.test('test SKIP comment', function (assert) { + assert.plan(1); + + var verify = function (output) { + assert.equal(output.toString('utf8'), [ + 'TAP version 13', + '# SKIP skipped', + '', + '1..0', + '# tests 0', + '# pass 0', + '', + '# ok', + '' + ].join('\n')); + }; + + var tapeTest = test.createHarness(); + tapeTest.createStream().pipe(concat(verify)); + tapeTest('skipped', { skip: true }, function (t) { + t.end(); + }); }); test('skip this', { skip: true }, function(t) { t.fail('this should not even run'); + ran++; + t.end(); +}); + +test.skip('skip this too', function(t) { + t.fail('this should not even run'); + ran++; t.end(); }); test('skip subtest', function(t) { - ran ++; - t.test('do not skip this', { skip: false }, function(t) { - ran ++; - t.pass('this should run'); - t.end(); - }); + ran++; t.test('skip this', { skip: true }, function(t) { t.fail('this should not even run'); t.end(); @@ -26,9 +49,4 @@ test('skip subtest', function(t) { t.end(); }); -test('right number of tests ran', function(t) { - t.equal(ran, 3, 'ran the right number of tests'); - t.end(); -}); - // vim: set softtabstop=4 shiftwidth=4: diff --git a/test/stackTrace.js b/test/stackTrace.js new file mode 100644 index 00000000..8da3b529 --- /dev/null +++ b/test/stackTrace.js @@ -0,0 +1,253 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var tapParser = require('tap-parser'); +var yaml = require('js-yaml'); + +tap.test('preserves stack trace with newlines', function (tt) { + tt.plan(3); + + var test = tape.createHarness(); + var stream = test.createStream(); + var parser = stream.pipe(tapParser()); + var stackTrace = 'foo\n bar'; + + parser.once('assert', function (data) { + delete data.diag.at; + tt.deepEqual(data, { + ok: false, + id: 1, + name: "Error: Preserve stack", + diag: { + stack: stackTrace, + operator: 'error', + expected: 'undefined', + actual: '[Error: Preserve stack]' + } + }); + }); + + stream.pipe(concat(function (body) { + var body = body.toString('utf8'); + body = stripAt(body); + tt.equal( + body, + 'TAP version 13\n' + + '# multiline stack trace\n' + + 'not ok 1 Error: Preserve stack\n' + + ' ---\n' + + ' operator: error\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + ' [Error: Preserve stack]\n' + + ' stack: |-\n' + + ' foo\n' + + ' bar\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + tt.deepEqual(getDiag(body), { + stack: stackTrace, + operator: 'error', + expected: 'undefined', + actual: '[Error: Preserve stack]' + }); + })); + + test('multiline stack trace', function (t) { + t.plan(1); + var err = new Error('Preserve stack'); + err.stack = stackTrace; + t.error(err); + }); +}); + +tap.test('parses function name from original stack', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + test.createStream(); + + test._results._watch = function (t) { + t.on('result', function (res) { + tt.equal('Test.testFunctionNameParsing', res.functionName) + }); + }; + + test('t.equal stack trace', function testFunctionNameParsing(t) { + t.equal(true, false, 'true should be false'); + t.end(); + }); +}); + +tap.test('parses function name from original stack for anonymous function', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + test.createStream(); + + test._results._watch = function (t) { + t.on('result', function (res) { + tt.equal('Test.', res.functionName) + }); + }; + + test('t.equal stack trace', function (t) { + t.equal(true, false, 'true should be false'); + t.end(); + }); +}); + +tap.test('preserves stack trace for failed assertions', function (tt) { + tt.plan(6); + + var test = tape.createHarness(); + var stream = test.createStream(); + var parser = stream.pipe(tapParser()); + + var stack = '' + parser.once('assert', function (data) { + tt.equal(typeof data.diag.at, 'string'); + tt.equal(typeof data.diag.stack, 'string'); + at = data.diag.at || ''; + stack = data.diag.stack || ''; + tt.ok(/^Error: true should be false(\n at .+)+/.exec(stack), 'stack should be a stack') + tt.deepEqual(data, { + ok: false, + id: 1, + name: "true should be false", + diag: { + at: at, + stack: stack, + operator: 'equal', + expected: false, + actual: true + } + }); + }); + + stream.pipe(concat(function (body) { + var body = body.toString('utf8'); + body = stripAt(body); + tt.equal( + body, + 'TAP version 13\n' + + '# t.equal stack trace\n' + + 'not ok 1 true should be false\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: false\n' + + ' actual: true\n' + + ' stack: |-\n' + + ' ' + + stack.replace(/\n/g, '\n ') + '\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + tt.deepEqual(getDiag(body), { + stack: stack, + operator: 'equal', + expected: false, + actual: true + }); + })); + + test('t.equal stack trace', function (t) { + t.plan(1); + t.equal(true, false, 'true should be false'); + }); +}); + +tap.test('preserves stack trace for failed assertions where actual===falsy', function (tt) { + tt.plan(6); + + var test = tape.createHarness(); + var stream = test.createStream(); + var parser = stream.pipe(tapParser()); + + var stack = '' + parser.once('assert', function (data) { + tt.equal(typeof data.diag.at, 'string'); + tt.equal(typeof data.diag.stack, 'string'); + at = data.diag.at || ''; + stack = data.diag.stack || ''; + tt.ok(/^Error: false should be true(\n at .+)+/.exec(stack), 'stack should be a stack') + tt.deepEqual(data, { + ok: false, + id: 1, + name: "false should be true", + diag: { + at: at, + stack: stack, + operator: 'equal', + expected: true, + actual: false + } + }); + }); + + stream.pipe(concat(function (body) { + var body = body.toString('utf8'); + body = stripAt(body); + tt.equal( + body, + 'TAP version 13\n' + + '# t.equal stack trace\n' + + 'not ok 1 false should be true\n' + + ' ---\n' + + ' operator: equal\n' + + ' expected: true\n' + + ' actual: false\n' + + ' stack: |-\n' + + ' ' + + stack.replace(/\n/g, '\n ') + '\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + + tt.deepEqual(getDiag(body), { + stack: stack, + operator: 'equal', + expected: true, + actual: false + }); + })); + + test('t.equal stack trace', function (t) { + t.plan(1); + t.equal(false, true, 'false should be true'); + }); +}); + +function getDiag (body) { + var yamlStart = body.indexOf(' ---'); + var yamlEnd = body.indexOf(' ...\n'); + var diag = body.slice(yamlStart, yamlEnd).split('\n').map(function (line) { + return line.slice(2); + }).join('\n'); + + // Get rid of 'at' variable (which has a line number / path of its own that's + // difficult to check). + var withStack = yaml.safeLoad(diag); + delete withStack.at; + return withStack; +} + +function stripAt (body) { + return body.replace(/^\s*at:\s+Test.*$\n/m, ''); +} diff --git a/test/subcount.js b/test/subcount.js new file mode 100644 index 00000000..3a5df3fb --- /dev/null +++ b/test/subcount.js @@ -0,0 +1,14 @@ +var test = require('../'); + +test('parent test', function (t) { + t.plan(2); + t.test('first child', function (t) { + t.plan(1); + t.pass('pass first child'); + }) + + t.test(function (t) { + t.plan(1); + t.pass('pass second child'); + }) +}) diff --git a/test/subtest_and_async.js b/test/subtest_and_async.js new file mode 100644 index 00000000..3703a979 --- /dev/null +++ b/test/subtest_and_async.js @@ -0,0 +1,25 @@ +var test = require('../'); + +var asyncFunction = function (callback) { + setTimeout(callback, Math.random * 50); +}; + +test('master test', function (t) { + t.test('subtest 1', function (st) { + st.pass('subtest 1 before async call'); + asyncFunction(function () { + st.pass('subtest 1 in async callback'); + st.end(); + }) + }); + + t.test('subtest 2', function (st) { + st.pass('subtest 2 before async call'); + asyncFunction(function () { + st.pass('subtest 2 in async callback'); + st.end(); + }) + }); + + t.end(); +}); diff --git a/test/subtest_plan.js b/test/subtest_plan.js new file mode 100644 index 00000000..2b075ae5 --- /dev/null +++ b/test/subtest_plan.js @@ -0,0 +1,21 @@ +var test = require('../'); + +test('parent', function (t) { + t.plan(3) + + var firstChildRan = false; + + t.pass('assertion in parent'); + + t.test('first child', function (t) { + t.plan(1); + t.pass('pass first child'); + firstChildRan = true; + }); + + t.test('second child', function (t) { + t.plan(2); + t.ok(firstChildRan, 'first child ran first'); + t.pass('pass second child'); + }); +}); diff --git a/test/throw.js b/test/throw.js deleted file mode 100644 index 32970b26..00000000 --- a/test/throw.js +++ /dev/null @@ -1,38 +0,0 @@ -var falafel = require('falafel'); -var tape = require('../'); -var tap = require('tap'); - -tap.test('throw test', function (tt) { - tt.plan(1); - - var test = tape.createHarness({ exit : false }); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - tt.same(rs, [ - 'TAP version 13', - 'thrower', - { id: 1, ok: true, name: 'should be equal' }, - { id: 2, ok: false, name: 'Error: rawr' }, - 'tests 2', - 'pass 1', - 'fail 1', - ]); - }); - - test.createStream().pipe(tc); - - test('thrower', function (t) { - t.equal(1 + 1, 2); - - throw new Error('rawr'); - }); -}); diff --git a/test/throws.js b/test/throws.js index 2192a7c0..9b2bba96 100644 --- a/test/throws.js +++ b/test/throws.js @@ -2,6 +2,8 @@ var tape = require('../'); var tap = require('tap'); var concat = require('concat-stream'); +var stripFullStack = require('./common').stripFullStack; + function fn() { throw new TypeError('RegExp'); } @@ -14,91 +16,163 @@ function getNonFunctionMessage(fn) { } } -tape('throws', function (t) { - t.throws(fn); - t.end(); -}); - -tape('throws (RegExp match)', function (t) { - t.throws(fn, /RegExp/); - t.end(); -}); - -tape('throws (Function match)', function (t) { - t.throws(fn, TypeError); - t.end(); -}); +var getter = function () { return 'message'; }; +var messageGetterError = Object.defineProperty( + { custom: 'error' }, + 'message', + { configurable: true, enumerable: true, get: getter } +); +var thrower = function () { throw messageGetterError; }; tap.test('failures', function (tt) { + tt.plan(1); + var test = tape.createHarness(); test.createStream().pipe(concat(function (body) { tt.equal( - body.toString('utf8'), + stripFullStack(body.toString('utf8')), 'TAP version 13\n' - + '# non functions\n' - + 'not ok 1 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage() + '"}\n' - + ' ...\n' - + 'not ok 2 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage(null) + '"}\n' - + ' ...\n' - + 'not ok 3 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage(true) + '"}\n' - + ' ...\n' - + 'not ok 4 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage(false) + '"}\n' - + ' ...\n' - + 'not ok 5 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage('abc') + '"}\n' - + ' ...\n' - + 'not ok 6 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage(/a/g) + '"}\n' - + ' ...\n' - + 'not ok 7 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage([]) + '"}\n' - + ' ...\n' - + 'not ok 8 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + " actual: {\"message\":\"" + getNonFunctionMessage({}) + '"}\n' - + ' ...\n' - + '# function\n' - + 'not ok 9 should throw\n' - + ' ---\n' - + ' operator: throws\n' - + ' expected: \n' - + ' actual: \n' - + ' ...\n\n' - + '1..9\n' - + '# tests 9\n' - + '# pass 0\n' - + '# fail 9\n' + + '# non functions\n' + + 'not ok 1 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage() + "] message: '" + getNonFunctionMessage() + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage(undefined) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 2 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage(null) + "] message: '" + getNonFunctionMessage(null) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage(null) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 3 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage(true) + "] message: '" + getNonFunctionMessage(true) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage(true) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 4 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage(false) + "] message: '" + getNonFunctionMessage(false) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage(false) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 5 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage('abc') + "] message: '" + getNonFunctionMessage('abc') + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage('abc') + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 6 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage(/a/g) + "] message: '" + getNonFunctionMessage(/a/g) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage(/a/g) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 7 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage([]) + "] message: '" + getNonFunctionMessage([]) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage([]) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + 'not ok 8 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: |-\n' + + ' undefined\n' + + ' actual: |-\n' + + " { [TypeError: " + getNonFunctionMessage({}) + "] message: '" + getNonFunctionMessage({}) + "' }\n" + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' TypeError: ' + getNonFunctionMessage({}) + '\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '# function\n' + + 'not ok 9 should throw\n' + + ' ---\n' + + ' operator: throws\n' + + ' expected: undefined\n' + + ' actual: undefined\n' + + ' at: Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should throw\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/throws.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '# custom error messages\n' + + 'ok 10 "message" is enumerable\n' + + "ok 11 { custom: 'error', message: 'message' }\n" + + 'ok 12 getter is still the same\n' + + '# throws null\n' + + 'ok 13 throws null\n' + + '\n1..13\n' + + '# tests 13\n' + + '# pass 4\n' + + '# fail 9\n' ); })); test('non functions', function (t) { + t.plan(8); t.throws(); t.throws(null); t.throws(true); @@ -107,13 +181,23 @@ tap.test('failures', function (tt) { t.throws(/a/g); t.throws([]); t.throws({}); - t.end(); }); test('function', function (t) { + t.plan(1); t.throws(function () {}); - t.end(); }); - tt.end(); + test('custom error messages', function (t) { + t.plan(3); + t.equal(Object.prototype.propertyIsEnumerable.call(messageGetterError, 'message'), true, '"message" is enumerable'); + t.throws(thrower, "{ custom: 'error', message: 'message' }"); + t.equal(Object.getOwnPropertyDescriptor(messageGetterError, 'message').get, getter, 'getter is still the same'); + }); + + test('throws null', function (t) { + t.plan(1); + t.throws(function () { throw null; }, 'throws null'); + t.end(); + }); }); diff --git a/test/timeout.js b/test/timeout.js new file mode 100644 index 00000000..9f4cd825 --- /dev/null +++ b/test/timeout.js @@ -0,0 +1,15 @@ +var test = require('../'); +var ran = 0; + +test('timeout', function(t) { + t.pass('this should run'); + ran++; + setTimeout(function () { + t.end(); + }, 100); +}); + +test('should still run', { timeout: 50 }, function(t) { + t.equal(ran, 1); + t.end(); +}); diff --git a/test/timeoutAfter.js b/test/timeoutAfter.js new file mode 100644 index 00000000..80752d28 --- /dev/null +++ b/test/timeoutAfter.js @@ -0,0 +1,36 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('timeoutAfter test', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var tc = function (rows) { + tt.same(stripFullStack(rows.toString('utf8')), [ + 'TAP version 13', + '# timeoutAfter', + 'not ok 1 test timed out after 1ms', + ' ---', + ' operator: fail', + ' stack: |-', + ' Error: test timed out after 1ms', + ' [... stack stripped ...]', + ' ...', + '', + '1..1', + '# tests 1', + '# pass 0', + '# fail 1' + ].join('\n') + '\n'); + }; + + test.createStream().pipe(concat(tc)); + + test('timeoutAfter', function (t) { + t.plan(1); + t.timeoutAfter(1); + }); +}); diff --git a/test/todo.js b/test/todo.js new file mode 100644 index 00000000..907eaeec --- /dev/null +++ b/test/todo.js @@ -0,0 +1,42 @@ +var tap = require('tap'); +var tape = require('../'); +var concat = require('concat-stream'); + +var common = require('./common'); +var stripFullStack = common.stripFullStack; + +tap.test('tape todo test', function (assert) { + var test = tape.createHarness({ exit: false }); + assert.plan(1); + + test.createStream().pipe(concat(function (body) { + assert.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# success\n' + + 'ok 1 this test runs\n' + + '# failure\n' + + 'not ok 2 should never happen # TODO\n' + + ' ---\n' + + ' operator: fail\n' + + ' at: Test. ($TEST/todo.js:$LINE:$COL)\n' + + ' ...\n' + + '\n' + + '1..2\n' + + '# tests 2\n' + + '# pass 2\n' + + '\n' + + '# ok\n' + ) + })); + + test('success', function (t) { + t.equal(true, true, 'this test runs'); + t.end(); + }); + + test('failure', { todo: true }, function (t) { + t.fail('should never happen'); + t.end(); + }); +}); diff --git a/test/too_many.js b/test/too_many.js index b5c38819..5c7e4b38 100644 --- a/test/too_many.js +++ b/test/too_many.js @@ -1,61 +1,70 @@ var falafel = require('falafel'); var tape = require('../'); var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; tap.test('array test', function (tt) { tt.plan(1); - + var test = tape.createHarness({ exit : false }); - var tc = tap.createConsumer(); - - var rows = []; - tc.on('data', function (r) { rows.push(r) }); - tc.on('end', function () { - var rs = rows.map(function (r) { - if (r && typeof r === 'object') { - return { id : r.id, ok : r.ok, name : r.name.trim() }; - } - else return r; - }); - tt.same(rs, [ + var tc = function (rows) { + tt.same(stripFullStack(rows.toString('utf8')), [ 'TAP version 13', - 'array', - { id: 1, ok: true, name: 'should be equivalent' }, - { id: 2, ok: true, name: 'should be equivalent' }, - { id: 3, ok: true, name: 'should be equivalent' }, - { id: 4, ok: true, name: 'should be equivalent' }, - { id: 5, ok: false, name: 'plan != count' }, - { id: 6, ok: true, name: 'should be equivalent' }, - 'tests 6', - 'pass 5', - 'fail 1' - ]); - }); - - test.createStream().pipe(tc); - + '# array', + 'ok 1 should be equivalent', + 'ok 2 should be equivalent', + 'ok 3 should be equivalent', + 'ok 4 should be equivalent', + 'not ok 5 plan != count', + ' ---', + ' operator: fail', + ' expected: 3', + ' actual: 4', + ' at: ($TEST/too_many.js:$LINE:$COL)', + ' stack: |-', + ' Error: plan != count', + ' [... stack stripped ...]', + ' at $TEST/too_many.js:$LINE:$COL', + ' at eval (eval at ($TEST/too_many.js:$LINE:$COL))', + ' at eval (eval at ($TEST/too_many.js:$LINE:$COL))', + ' at Test. ($TEST/too_many.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + 'ok 6 should be equivalent', + '', + '1..6', + '# tests 6', + '# pass 5', + '# fail 1' + ].join('\n') + '\n'); + }; + + test.createStream().pipe(concat(tc)); + test('array', function (t) { t.plan(3); - + var src = '(' + function () { var xs = [ 1, 2, [ 3, 4 ] ]; var ys = [ 5, 6 ]; g([ xs, ys ]); } + ')()'; - + var output = falafel(src, function (node) { if (node.type === 'ArrayExpression') { node.update('fn(' + node.source() + ')'); } }); - + var arrays = [ [ 3, 4 ], [ 1, 2, [ 3, 4 ] ], [ 5, 6 ], [ [ 1, 2, [ 3, 4 ] ], [ 5, 6 ] ], ]; - + Function(['fn','g'], output)( function (xs) { t.same(arrays.shift(), xs); diff --git a/test/undef.js b/test/undef.js new file mode 100644 index 00000000..52759ac7 --- /dev/null +++ b/test/undef.js @@ -0,0 +1,42 @@ +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('array test', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + test.createStream().pipe(concat(function (body) { + tt.equal( + stripFullStack(body.toString('utf8')), + 'TAP version 13\n' + + '# undef\n' + + 'not ok 1 should be equivalent\n' + + ' ---\n' + + ' operator: deepEqual\n' + + ' expected: |-\n' + + ' { beep: undefined }\n' + + ' actual: |-\n' + + ' {}\n' + + ' at: Test. ($TEST/undef.js:$LINE:$COL)\n' + + ' stack: |-\n' + + ' Error: should be equivalent\n' + + ' [... stack stripped ...]\n' + + ' at Test. ($TEST/undef.js:$LINE:$COL)\n' + + ' [... stack stripped ...]\n' + + ' ...\n' + + '\n' + + '1..1\n' + + '# tests 1\n' + + '# pass 0\n' + + '# fail 1\n' + ); + })); + + test('undef', function (t) { + t.plan(1); + t.deepEqual({}, { beep: undefined }); + }); +});