diff --git a/bin/supertape.js b/bin/supertape.js index 27c0ed7..1d2a00a 100755 --- a/bin/supertape.js +++ b/bin/supertape.js @@ -4,11 +4,11 @@ const cli = require('../lib/cli.js'); -const {stdout} = process; -const write = stdout.write.bind(stdout); +const {stdout, exit} = process; module.exports = cli({ - write, + stdout, + exit, cwd: process.cwd(), argv: process.argv.slice(2), }); diff --git a/lib/cli.js b/lib/cli.js index 04aed6d..1ea2d61 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,12 +1,13 @@ 'use strict'; const {resolve: resolvePath} = require('path'); +const {once} = require('events'); const yargsParser = require('yargs-parser'); const glob = require('glob'); const fullstore = require('fullstore'); -const supertape = require('../lib/supertape.js'); +const supertape = require('..'); const {resolve} = require; const {isArray} = Array; @@ -15,13 +16,14 @@ const maybeArray = (a) => isArray(a) ? a : [a]; const removeDuplicates = (a) => Array.from(new Set(a)); const filesCount = fullstore(0); -module.exports = async ({argv, cwd, write}) => { +module.exports = async ({argv, cwd, stdout, exit}) => { const args = yargsParser(argv, { coerce: { require: maybeArray, }, string: [ 'require', + 'formatter', ], boolean: [ 'version', @@ -29,14 +31,16 @@ module.exports = async ({argv, cwd, write}) => { alias: { r: 'require', v: 'version', + f: 'formatter', }, default: { require: [], + formatter: 'tap', }, }); if (args.version) - return write(`v${require('../package').version}\n`); + return stdout.write(`v${require('../package').version}\n`); for (const module of args.require) { await import(resolve(module, { @@ -44,10 +48,16 @@ module.exports = async ({argv, cwd, write}) => { })); } + const {formatter} = args; + supertape.init({ - loop: false, + run: false, + quiet: true, + formatter, }); + supertape.createStream().pipe(stdout); + const allFiles = []; for (const arg of args._) { const files = glob.sync(arg); @@ -63,10 +73,15 @@ module.exports = async ({argv, cwd, write}) => { filesCount(files.length); + let code = 0; + if (promises.length) { await Promise.all(promises); - supertape.loop(); + const [{failed}] = await once(supertape.run(), 'end'); + code = failed ? 1 : 0; } + + exit(code); }; module.exports._filesCount = filesCount; diff --git a/lib/cli.spec.js b/lib/cli.spec.js index 90d940f..8df529a 100644 --- a/lib/cli.spec.js +++ b/lib/cli.spec.js @@ -1,10 +1,14 @@ 'use strict'; const {join} = require('path'); +const {Transform} = require('stream'); +const {EventEmitter} = require('events'); const stub = require('@cloudcmd/stub'); const mockRequire = require('mock-require'); const tryToCatch = require('try-to-catch'); +const pullout = require('pullout'); +const wait = require('@iocmd/wait'); const test = require('./supertape.js'); const cli = require('./cli'); @@ -15,9 +19,8 @@ const {assign} = Object; test('supertape: cli: -r', async (t) => { const argv = ['-r', 'hello']; - const [error] = await tryToCatch(cli, { + const [error] = await runCli({ argv, - cwd: __dirname, }); t.ok(error.message.includes(`Cannot find module 'hello'`)); @@ -27,15 +30,17 @@ test('supertape: cli: -r', async (t) => { test('supertape: cli: -v', async (t) => { const {version} = require('../package'); const argv = ['-v']; - const write = stub(); + const stdout = createStream(); await cli({ argv, - cwd: __dirname, - write, + stdout, }); - t.ok(write.calledWith(`v${version}\n`)); + stdout.push(null); + const result = await pullout(stdout); + + t.equal(result, `v${version}\n`); t.end(); }); @@ -47,10 +52,8 @@ test('supertape: bin: cli: glob', async (t) => { sync, }); - const cli = reRequire('./cli'); - await cli({ + await runCli({ argv, - cwd: __dirname, }); stopAll(); @@ -67,10 +70,8 @@ test('supertape: bin: cli: glob: a couple', async (t) => { sync, }); - const cli = reRequire('./cli'); - await cli({ + await runCli({ argv, - cwd: __dirname, }); stopAll(); @@ -81,85 +82,159 @@ test('supertape: bin: cli: glob: a couple', async (t) => { t.end(); }); -test('supertape: bin: cli: loop', async (t) => { +test('supertape: bin: cli: run', async (t) => { const name = join(__dirname, 'fixture/cli.js'); const argv = [name, name]; const test = stub(); const init = stub(); - const loop = stub(); + const run = stub(); + + const {createStream} = reRequire('..'); assign(test, { init, - loop, + run, + createStream, }); - mockRequire('./supertape', test); - const cli = reRequire('./cli'); - await cli({ + mockRequire('..', test); + + await runCli({ argv, - cwd: __dirname, }); stopAll(); - t.equal(loop.callCount, 1, 'loop always called once'); + t.equal(run.callCount, 1, 'run always called once'); t.end(); }); -test('supertape: bin: cli: loop', async (t) => { +test('supertape: bin: cli: files count', async (t) => { const name = join(__dirname, 'fixture/cli.js'); const argv = [name, name]; + const emitter = new EventEmitter(); + const test = stub(); const init = stub(); - const loop = stub(); + const run = stub().returns(emitter); assign(test, { init, - loop, + run, + createStream, }); + mockRequire('./supertape', test); - const cli = reRequire('./cli'); - await cli({ + const emit = emitter.emit.bind(emitter); + const [[error, cli]] = await Promise.all([ + runCli({ + argv, + }), + wait(emit, 'end', {failed: 0}), + ]); + + const result = cli._filesCount(); + const expected = 1; + + stopAll(); + + t.equal(result, expected, 'should process 1 file'); + t.end(); +}); + +test('supertape: bin: cli: successs', async (t) => { + const name = join(__dirname, 'fixture/cli-success.js'); + const argv = [name, name]; + + const supertape = reRequire('./supertape'); + const init = stub(); + const exit = stub(); + + mockRequire('supertape', { + ...supertape, + init, + }); + + supertape.init({ + quiet: true, + }); + + await runCli({ argv, - cwd: __dirname, + exit, }); stopAll(); - t.equal(loop.callCount, 1, 'test always called once'); + t.ok(exit.calledWith(0), 'should call exit with 0'); t.end(); }); -test('supertape: bin: cli: files count', async (t) => { +test('supertape: bin: cli: fail', async (t) => { const name = join(__dirname, 'fixture/cli.js'); const argv = [name, name]; - const test = stub(); const init = stub(); - const loop = stub(); + const exit = stub(); + + const test = stub(); + const emitter = new EventEmitter(); + const run = stub().returns(emitter); assign(test, { init, - loop, + createStream, + run, }); - mockRequire('./supertape', test); + mockRequire('..', test); - const cli = reRequire('./cli'); - await cli({ - argv, - cwd: __dirname, - }); - - const result = cli._filesCount(); - const expected = 1; + const emit = emitter.emit.bind(emitter); + await Promise.all([ + runCli({ + argv, + exit, + }), + wait(emit, 'end', { + failed: 1, + }), + ]); stopAll(); - t.equal(result, expected, 'should process 1 file'); + t.ok(exit.calledWith(1), 'should call exit with 1'); t.end(); }); +function createStream() { + return new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk); + callback(); + }, + }); +} + +async function runCli(options) { + const { + argv = [], + stdout = createStream(), + cwd = __dirname, + exit = stub(), + } = options; + + const cli = reRequire('./cli'); + + const [error] = await tryToCatch(cli, { + argv, + stdout, + cwd, + exit, + }); + + return [error, cli]; +} + diff --git a/lib/diff.spec.js b/lib/diff.spec.js index 583d5e3..0aeb8d2 100644 --- a/lib/diff.spec.js +++ b/lib/diff.spec.js @@ -8,7 +8,7 @@ test('supertape: diff', (t) => { const {length} = diffed.split('\n'); const expected = 2; - t.equal(expected, length); + t.equal(length, expected); t.end(); }); diff --git a/lib/fixture/cli-success.js b/lib/fixture/cli-success.js new file mode 100644 index 0000000..27132ec --- /dev/null +++ b/lib/fixture/cli-success.js @@ -0,0 +1,7 @@ +const test = require('../supertape'); + +test('hello world', (t) => { + t.equal(1, 1); + t.end(); +}); + diff --git a/lib/fixture/cli.js b/lib/fixture/cli.js index e81adf6..43a07e4 100644 --- a/lib/fixture/cli.js +++ b/lib/fixture/cli.js @@ -1,6 +1,7 @@ const test = require('../supertape'); test('hello world', (t) => { + t.equal(1, 2); t.end(); }); diff --git a/lib/operators.js b/lib/operators.js index 2087835..ced1e3d 100644 --- a/lib/operators.js +++ b/lib/operators.js @@ -5,10 +5,9 @@ const deepEqualCheck = require('deep-equal'); const {formatOutput, parseAt} = require('./format'); const diff = require('./diff'); -const isStr = (a) => typeof a === 'string'; const {entries} = Object; -// backward compatibility or maybe emitters support +// backward compatibility or maybe reporters support const end = () => {}; const ok = (actual, message = 'should be truthy') => { @@ -98,7 +97,7 @@ const notDeepEqual = (actual, expected, message = 'should not deep equal') => { }; }; -const comment = ({out}) => (message) => { +const comment = ({reporter}) => (message) => { const messages = message.trim().split('\n'); for (const current of messages) { @@ -106,7 +105,7 @@ const comment = ({out}) => (message) => { .trim() .replace(/^#\s*/, ''); - out(`# ${line}`); + reporter.emit('comment', line); } }; @@ -122,7 +121,7 @@ const operators = { end, }; -const initOperator = ({out, count, incCount, incPassed, incFailed}) => (name) => (...a) => { +const initOperator = ({reporter, count, incCount, incPassed, incFailed}) => (name) => (...a) => { const { is, message, @@ -136,39 +135,35 @@ const initOperator = ({out, count, incCount, incPassed, incFailed}) => (name) => if (is) { incPassed(); - out(`ok ${count()} ${message}`); + reporter.emit('test:success', { + count: count(), + message, + }); return; } incFailed(); - out(`not ok ${count()} ${message}`); - out(' ---'); - out(` operator: ${name}`); - - if (output) - out(output); - - if (!isStr(output)) { - out(' expected: |-'); - out(` ${expected}`); - out(' actual: |-'); - out(` ${actual}`); - } - const errorStack = stack || Error(message).stack; const reason = stack ? 'user' : 'assert'; - out(` ${parseAt(errorStack, {reason})}`); - out(' stack: |-'); - out(formatOutput(errorStack)); + reporter.emit('test:fail', { + count: count(), + message, + operator: name, + actual, + expected, + output, + errorStack: formatOutput(errorStack), + at: parseAt(errorStack, {reason}), + }); }; module.exports.operators = operators; -module.exports.initOperators = ({out, count, incCount, incPassed, incFailed, extensions}) => { +module.exports.initOperators = ({reporter, count, incCount, incPassed, incFailed, extensions}) => { const operator = initOperator({ - out, + reporter, count, incCount, incPassed, @@ -191,7 +186,7 @@ module.exports.initOperators = ({out, count, incCount, incPassed, incFailed, ext notOk: operator('notOk'), pass: operator('pass'), fail: operator('fail'), - comment: comment({out}), + comment: comment({reporter}), end, ...extendedOperators, diff --git a/lib/operators.spec.js b/lib/operators.spec.js index c967ba5..7a06b95 100644 --- a/lib/operators.spec.js +++ b/lib/operators.spec.js @@ -1,57 +1,71 @@ 'use strict'; -const test = require('..'); +const {once, EventEmitter} = require('events'); const stub = require('@cloudcmd/stub'); +const test = require('..'); const { initOperators, operators, } = require('./operators'); -const {createOutput} = require('./supertape'); -test('supertape: operators: extendOperators', (t) => { - const out = createOutput(); - +test('supertape: operators: extendOperators', async (t) => { const extensions = { transformCode: (t) => (a, b) => { return t.equal(a, b, 'should transform code'); }, }; - const {transformCode} = initOperators(getStubs({out, extensions})); + const reporter = new EventEmitter(); + const {transformCode} = initOperators(getStubs({reporter, extensions})); - transformCode('a', 'a'); + const [[result]] = await Promise.all([ + once(reporter, 'test:success'), + transformCode('a', 'a'), + ]); - const result = out(); - const expected = 'ok 1 should transform code'; + const expected = { + count: 1, + message: 'should transform code', + }; - t.equal(result, expected); + t.deepEqual(result, expected); t.end(); }); -test('supertape: operators: initOperators: notEqual', (t) => { - const out = createOutput(); - const {notEqual} = initOperators(getStubs({out})); - - notEqual(+0, -0); - - const result = out(); - const expected = 'ok 1 should not equal'; +test('supertape: operators: initOperators: notEqual', async (t) => { + const reporter = new EventEmitter(); + const {notEqual} = initOperators(getStubs({reporter})); + + const [[result]] = await Promise.all([ + once(reporter, 'test:success'), + notEqual(+0, -0), + ]); + + const expected = { + count: 1, + message: 'should not equal', + }; - t.equal(result, expected); + t.deepEqual(result, expected); t.end(); }); -test('supertape: operators: initOperators: notDeepEqual: true', (t) => { - const out = createOutput(); - const {notDeepEqual} = initOperators(getStubs({out})); - - notDeepEqual({a: 'b'}, {b: 'a'}); - - const result = out(); - const expected = 'ok 1 should not deep equal'; +test('supertape: operators: initOperators: notDeepEqual: true', async (t) => { + const reporter = new EventEmitter(); + const {notDeepEqual} = initOperators(getStubs({reporter})); + + const [[result]] = await Promise.all([ + once(reporter, 'test:success'), + notDeepEqual({a: 'b'}, {b: 'a'}), + ]); + + const expected = { + count: 1, + message: 'should not deep equal', + }; - t.equal(result, expected); + t.deepEqual(result, expected); t.end(); }); @@ -97,7 +111,7 @@ test('supertape: operators: notDeepEqual: true', (t) => { function getStubs(stubs = {}) { const { - out = stub(), + reporter = new EventEmitter(), count = stub().returns(1), incCount = stub(), incPassed = stub(), @@ -106,7 +120,7 @@ function getStubs(stubs = {}) { } = stubs; return { - out, + reporter, count, incCount, incPassed, diff --git a/lib/reporter/fail.js b/lib/reporter/fail.js new file mode 100644 index 0000000..a494a72 --- /dev/null +++ b/lib/reporter/fail.js @@ -0,0 +1,32 @@ +'use strict'; + +const tap = require('./tap'); +const { + start, + comment, + end, +} = tap; + +const fullstore = require('fullstore'); +const testStore = fullstore(); + +function test({message}) { + testStore(message); + return ''; +} + +function fail(...a) { + const message = testStore(); + const fail = tap.fail(...a); + + return `# ${message}\n${fail}`; +} + +module.exports = { + start, + test, + comment, + fail, + end, +}; + diff --git a/lib/reporter/fail.spec.js b/lib/reporter/fail.spec.js new file mode 100644 index 0000000..ddf6433 --- /dev/null +++ b/lib/reporter/fail.spec.js @@ -0,0 +1,64 @@ +'use strict'; + +const {once} = require('events'); + +const montag = require('montag'); +const {reRequire} = require('mock-require'); +const pullout = require('pullout'); + +const test = require('../..'); + +const pull = async (stream, i = 9) => { + const output = await pullout(stream); + + return output.split('\n') + .slice(0, i) + .join('\n'); +}; + +test('supertape: formatters: fail', async (t) => { + const successFn = (t) => { + t.ok(true); + t.end(); + }; + + const successMessage = 'success'; + + const failFn = (t) => { + t.ok(false); + t.end(); + }; + + const failMessage = 'fail'; + + const supertape = reRequire('../..'); + + supertape.init({ + quiet: true, + formatter: 'fail', + }); + + supertape(successMessage, successFn); + supertape(failMessage, failFn); + + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); + + const expected = montag` + TAP version 13 + # fail + not ok 2 should be truthy + --- + operator: ok + expected: |- + true + actual: |- + false + `; + + t.equal(result, expected); + t.end(); +}); + diff --git a/lib/reporter/harness.js b/lib/reporter/harness.js new file mode 100644 index 0000000..994ca11 --- /dev/null +++ b/lib/reporter/harness.js @@ -0,0 +1,66 @@ +'use strict'; + +const {Transform} = require('stream'); + +const {assign} = Object; + +module.exports.createHarness = (reporter) => { + const prepared = prepare(reporter); + + const stream = new Transform({ + readableObjectMode: true, + writableObjectMode: true, + + transform(chunk, encoding, callback) { + const {type, ...data} = chunk; + const result = process(prepared, type, data); + + this.push(result); + + if (type === 'end') + this.push(null); + + callback(); + }, + }); + + return stream; +}; + +const stub = () => {}; + +function prepare(reporter) { + const result = {}; + + assign(result, { + start: stub, + test: stub, + fail: stub, + success: stub, + comment: stub, + end: stub, + ...reporter, + }); + + return result; +} + +function process(reporter, type, data) { + if (type === 'start') + return reporter.start(); + + if (type === 'test') + return reporter.test(data); + + if (type === 'comment') + return reporter.comment(data); + + if (type === 'success') + return reporter.success(data); + + if (type === 'fail') + return reporter.fail(data); + + return reporter.end(data); +} + diff --git a/lib/reporter/index.js b/lib/reporter/index.js new file mode 100644 index 0000000..9337a8d --- /dev/null +++ b/lib/reporter/index.js @@ -0,0 +1,64 @@ +'use strict'; + +const {EventEmitter} = require('events'); +const {createHarness} = require('./harness'); + +const resolveFormatter = (name) => { + return require(`${__dirname}/${name}`); +}; + +module.exports.createReporter = (name) => { + const reporter = new EventEmitter(); + const formatter = resolveFormatter(name); + const harness = createHarness(formatter); + + reporter.on('start', () => { + harness.write({ + type: 'start', + }); + }); + + reporter.on('test', (message) => { + harness.write({ + type: 'test', + message, + }); + }); + + reporter.on('comment', (message) => { + harness.write({ + type: 'comment', + message, + }); + }); + + reporter.on('test:success', ({count, message}) => { + harness.write({ + type: 'success', + count, + message, + }); + }); + + reporter.on('test:fail', ({is, at, count, message, operator, actual, expected, output, errorStack}) => { + harness.write({ + type: 'fail', + is, at, count, message, operator, actual, expected, output, errorStack, + }); + }); + + reporter.on('end', ({count, passed, failed}) => { + harness.write({ + type: 'end', + count, + passed, + failed, + }); + }); + + return { + reporter, + harness, + }; +}; + diff --git a/lib/reporter/tap.js b/lib/reporter/tap.js new file mode 100644 index 0000000..ba833e8 --- /dev/null +++ b/lib/reporter/tap.js @@ -0,0 +1,84 @@ +'use strict'; + +const isStr = (a) => typeof a === 'string'; + +module.exports.start = () => { + return 'TAP version 13\n'; +}; + +module.exports.test = ({message}) => { + return `# ${message}\n`; +}; + +module.exports.comment = ({message}) => { + return `# ${message}\n`; +}; + +module.exports.success = ({count, message}) => { + return `ok ${count} ${message}\n`; +}; + +module.exports.fail = ({at, count, message, operator, actual, expected, output, errorStack}) => { + const out = createOutput(); + + out(`not ok ${count} ${message}`); + out(' ---'); + out(` operator: ${operator}`); + + if (output) + out(output); + + if (!isStr(output)) { + out(' expected: |-'); + out(` ${expected}`); + out(' actual: |-'); + out(` ${actual}`); + } + + out(` ${at}`); + out(' stack: |-'); + out(errorStack); + out(' ...'); + out(''); + + return out(); +}; + +module.exports.end = ({count, passed, failed}) => { + const out = createOutput(); + + out(''); + + out(`1..${count}`); + out(`# tests ${count}`); + out(`# pass ${passed}`); + + if (failed) { + out(`# fail ${failed}`); + } + + out(''); + + if (!failed) { + out('# ok'); + out(''); + } + + out(''); + + return out(); +}; + +function createOutput() { + const output = []; + + return (...args) => { + const [line] = args; + + if (!args.length) + return output.join('\n'); + + output.push(line); + }; +} + diff --git a/lib/run-tests.js b/lib/run-tests.js index 35b34b5..b37b2de 100644 --- a/lib/run-tests.js +++ b/lib/run-tests.js @@ -8,7 +8,7 @@ const {initOperators} = require('./operators'); const inc = wraptile((store) => store(store() + 1)); -module.exports = async function runTests(tests, {out}) { +module.exports = async function runTests(tests, {reporter}) { const count = fullstore(0); const failed = fullstore(0); const passed = fullstore(0); @@ -17,41 +17,39 @@ module.exports = async function runTests(tests, {out}) { const incFailed = inc(failed); const incPassed = inc(passed); - out('TAP version 13'); + reporter.emit('start'); - for (const {fn, message, extensions = {}} of tests) + for (const {fn, message, extensions} of tests) { await runOneTest({ message, fn, - out, + reporter, count, incCount, incFailed, incPassed, extensions, }); + } - out(''); - out(`1..${count()}`); - out(`# tests ${count()}`); - out(`# pass ${passed()}`); - - if (failed()) - out(`# fail ${failed()}`); - - out(''); - - if (!failed()) - out('# ok'); + reporter.emit('end', { + count: count(), + passed: passed(), + failed: failed(), + }); - out(''); + return { + count: count(), + failed: failed(), + passed: passed(), + }; }; -async function runOneTest({message, fn, extensions, out, count, incCount, incPassed, incFailed}) { - out(`# ${message}`); +async function runOneTest({message, fn, extensions, reporter, count, incCount, incPassed, incFailed}) { + reporter.emit('test', message); const t = initOperators({ - out, + reporter, count, incCount, incPassed, diff --git a/lib/run-tests.spec.js b/lib/run-tests.spec.js index 8120d9e..b1e409e 100644 --- a/lib/run-tests.spec.js +++ b/lib/run-tests.spec.js @@ -1,34 +1,38 @@ 'use strict'; +const {once} = require('events'); + const montag = require('montag'); const {reRequire} = require('mock-require'); +const pullout = require('pullout'); const test = require('..'); -const {createOutput} = test; - -const cutStackTrace = (a, i = 9) => a - .split('\n') - .slice(0, i) - .join('\n'); +const pull = async (stream, i = 9) => { + const output = await pullout(stream); + + return output.split('\n') + .slice(0, i) + .join('\n'); +}; test('supertape: runTests', async (t) => { - const out = createOutput(); const fn = (t) => { t.ok(false); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out()); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -47,22 +51,22 @@ test('supertape: runTests', async (t) => { }); test('supertape: runTests: fail', async (t) => { - const out = createOutput(); const fn = (t) => { t.fail('hello'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out(), 5); + const [result] = await Promise.all([ + pull(supertape.createStream(), 5), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -77,22 +81,22 @@ test('supertape: runTests: fail', async (t) => { }); test('supertape: runTests: equal', async (t) => { - const out = createOutput(); const fn = (t) => { t.equal('hello', 'hello'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = out(); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -104,7 +108,6 @@ test('supertape: runTests: equal', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -112,23 +115,23 @@ test('supertape: runTests: equal', async (t) => { }); test('supertape: runTests: not equal', async (t) => { - const out = createOutput(); const fn = (t) => { t.equal('hello', 'world'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); const BEFORE_DIFF = 6; - const result = cutStackTrace(out(), BEFORE_DIFF); + const [result] = await Promise.all([ + pull(supertape.createStream(), BEFORE_DIFF), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -144,23 +147,23 @@ test('supertape: runTests: not equal', async (t) => { }); test('supertape: runTests: not deepEqual', async (t) => { - const out = createOutput(); const fn = (t) => { t.deepEqual('hello', 'world'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); const BEFORE_DIFF = 6; - const result = cutStackTrace(out(), BEFORE_DIFF); + const [result] = await Promise.all([ + pull(supertape.createStream(), BEFORE_DIFF), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -176,22 +179,22 @@ test('supertape: runTests: not deepEqual', async (t) => { }); test('supertape: runTests: comment', async (t) => { - const out = createOutput(); const fn = (t) => { t.comment('hello'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out()); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -210,21 +213,21 @@ test('supertape: runTests: comment', async (t) => { }); test('supertape: runTests: crash', async (t) => { - const out = createOutput(); const fn = () => { throw Error('x'); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out(), 3); + const [result] = await Promise.all([ + pull(supertape.createStream(), 3), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -237,22 +240,22 @@ test('supertape: runTests: crash', async (t) => { }); test('supertape: runTests: pass', async (t) => { - const out = createOutput(); const fn = (t) => { t.pass('hello'); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out()); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -271,22 +274,22 @@ test('supertape: runTests: pass', async (t) => { }); test('supertape: runTests: pass: unnamed', async (t) => { - const out = createOutput(); const fn = (t) => { t.pass(); t.end(); }; const message = 'hello world'; - const tests = [{ - fn, - message, - }]; - const runTests = reRequire('./run-tests'); - await runTests(tests, {out}); + const supertape = reRequire('..'); + await supertape(message, fn, { + quiet: true, + }); - const result = cutStackTrace(out()); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 diff --git a/lib/stream.js b/lib/stream.js deleted file mode 100644 index 84540ea..0000000 --- a/lib/stream.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const {Readable} = require('stream'); - -module.exports.TapeReader = class TapeReader extends Readable { - constructor(options) { - super(options); - const {emitter} = options; - - this._emitter = emitter; - } - - _read() { - this._emitter.on('line', (line) => { - this.push(line); - }); - - this._emitter.on('end', () => { - this.push(null); - }); - } -}; diff --git a/lib/supertape.js b/lib/supertape.js index 963e2b2..b4ae1a5 100644 --- a/lib/supertape.js +++ b/lib/supertape.js @@ -4,44 +4,35 @@ const {EventEmitter} = require('events'); const once = require('once'); const runTests = require('./run-tests'); -const {TapeReader} = require('./stream'); +const reporter = require('./reporter'); + +const createReporter = once(reporter.createReporter); const createEmitter = once(_createEmitter); const {assign} = Object; - -const noop = () => {}; -const streamStub = { - write: () => {}, -}; +const {stdout} = process; const defaultOptions = { skip: false, only: false, extensions: {}, + quiet: false, + formatter: 'tap', + run: true, }; -const createOutput = ({emit = noop, output = []} = {}) => { - return (...args) => { - const [line] = args; - - if (!args.length) - return output.join('\n'); - - emit('line', `${line}\n`); - output.push(line); - }; -}; - -function _createEmitter({stream}) { +function _createEmitter({quiet, formatter}) { const tests = []; - const output = []; const emitter = new EventEmitter(); - const emit = emitter.emit.bind(emitter); - const out = createOutput({emit, output}); - emitter.on('test', (message, fn, {skip, only, extensions} = defaultOptions) => { + const {harness, reporter} = createReporter(formatter); + + if (!quiet) + harness.pipe(stdout); + + emitter.on('test', (message, fn, {skip, only, extensions}) => { tests.push({ message, fn, @@ -58,17 +49,12 @@ function _createEmitter({stream}) { }); }); - emitter.on('line', (line) => { - stream.write(line); - }); - emitter.on('run', async () => { - await run(tests, { - out, + const {failed} = await run(tests, { + reporter, }); - emitter.emit('result', out()); - emitter.emit('end'); + emitter.emit('end', {failed}); }); return emitter; @@ -76,31 +62,28 @@ function _createEmitter({stream}) { module.exports = test; -const initedOptions = {}; +const initedOptions = { + formatter: 'tap', +}; module.exports.init = (options) => { assign(initedOptions, options); }; -module.exports.createEmitter = () => { - const stream = streamStub; +const createStream = () => { + const {formatter} = initedOptions; + const {harness} = createReporter(formatter); - return _createEmitter({ - stream, - }); + return harness; }; -module.exports.createStream = (emitter) => { - return new TapeReader({ - emitter, - }); -}; +module.exports.createStream = createStream; function test(message, fn, options = {}) { const { - stream = process.stdout, - quiet = false, - loop = true, + run, + quiet, + formatter, only, skip, extensions, @@ -111,7 +94,8 @@ function test(message, fn, options = {}) { }; const emitter = createEmitter({ - stream: quiet ? streamStub : stream, + formatter, + quiet, }); emitter.emit('test', message, fn, { @@ -120,7 +104,7 @@ function test(message, fn, options = {}) { extensions, }); - if (loop) + if (run) emitter.emit('loop'); return emitter; @@ -183,27 +167,34 @@ const loop = once(({emitter, tests}) => { })(); }); -module.exports.loop = () => { - const emitter = createEmitter({}); +module.exports.run = () => { + const emitter = createEmitter(); emitter.emit('loop'); return emitter; }; -module.exports.runTests = runTests; -module.exports.createOutput = createOutput; - -async function run(tests, {out}) { +async function run(tests, {reporter}) { const onlyTests = tests.filter(isOnly); if (onlyTests.length) - return await runTests(onlyTests, {out}); + return await runTests(onlyTests, {reporter}); const notSkipedTests = tests.filter(notSkip); - if (!notSkipedTests.length) - return; + if (!notSkipedTests.length) { + reporter.emit('start'); + reporter.emit('end', { + count: 0, + failed: 0, + passed: 0, + }); + + return { + failed: 0, + }; + } - await runTests(notSkipedTests, {out}); + return await runTests(notSkipedTests, {reporter}); } diff --git a/lib/supertape.spec.js b/lib/supertape.spec.js index 404621d..48f359c 100644 --- a/lib/supertape.spec.js +++ b/lib/supertape.spec.js @@ -1,15 +1,17 @@ 'use strict'; const {once} = require('events'); +const {Transform} = require('stream'); const test = require('..'); -const stub = require('@cloudcmd/stub'); -const wait = require('@iocmd/wait'); const montag = require('montag'); const {reRequire} = require('mock-require'); const pullout = require('pullout'); -const {createOutput} = test; +const pull = async (stream) => { + const output = await pullout(stream); + return output.slice(0, -2); +}; test('supertape: equal', async (t) => { const fn = (t) => { @@ -18,19 +20,21 @@ test('supertape: equal', async (t) => { }; const message = 'hello'; - const tests = [{ - message, - fn, - }]; - const emit = stub(); - const out = createOutput({emit}); + const supertape = reRequire('..'); - const runTests = reRequire('./run-tests'); - await runTests(tests, { - out, + supertape.init({ + run: false, + quiet: true, }); - const result = out(); + + supertape(message, fn); + const stream = supertape.createStream(); + + const [result] = await Promise.all([ + pull(stream), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -42,7 +46,6 @@ test('supertape: equal', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -63,19 +66,21 @@ test('supertape: deepEqual', async (t) => { }; const message = 'hello'; - const tests = [{ - message, - fn, - }]; - const runTests = reRequire('./run-tests'); - const {createOutput} = reRequire('..'); + const supertape = reRequire('./supertape'); - const emit = stub(); - const out = createOutput({emit}); + supertape.init({ + run: false, + quiet: true, + }); - await runTests(tests, {out}); - const result = out(); + supertape(message, fn); + const stream = supertape.createStream(); + + const [result] = await Promise.all([ + pull(stream), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -87,71 +92,33 @@ test('supertape: deepEqual', async (t) => { # pass 1 # ok - `; t.equal(result, expected); t.end(); }); -test('supertape', async (t) => { +test('supertape: createStream', async (t) => { const fn = (t) => { - t.ok(true); + t.notOk(false); t.end(); }; const message = 'hello'; const supertape = reRequire('..'); - const emitter = supertape(message, fn, { + supertape.init({ quiet: true, }); - const [result] = await once(emitter, 'result'); - - const expected = montag` - TAP version 13 - # hello - ok 1 should be truthy - - 1..1 - # tests 1 - # pass 1 - - # ok - - `; - - t.equal(result, expected); - t.end(); -}); - -test('supertape: loop', async (t) => { - const {loop} = reRequire('..'); - - const emitter = loop(); + const stream = supertape.createStream(); - const [result] = await once(emitter, 'result'); + supertape(message, fn); - t.notOk(result, 'should emit not output'); - t.end(); -}); - -test('supertape: createEmitter', async (t) => { - const fn = (t) => { - t.notOk(false); - t.end(); - }; - - const message = 'hello'; - const {createEmitter} = reRequire('..'); - const emitter = createEmitter(); - const emit = emitter.emit.bind(emitter); - - emit('test', message, fn); - emit('run'); - - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(stream), + once(supertape.run(), 'end'), + ]); const expected = montag` TAP version 13 @@ -163,68 +130,44 @@ test('supertape: createEmitter', async (t) => { # pass 1 # ok - `; t.equal(result, expected); t.end(); }); -test('supertape: createStream', async (t) => { +test('supertape: skip', async (t) => { const fn = (t) => { - t.notOk(false); + t.ok(true); t.end(); }; const message = 'hello'; - const {createEmitter, createStream} = reRequire('..'); + const supertape = reRequire('..'); - const emitter = createEmitter(); - const stream = createStream(emitter); + const emitter = supertape.skip(message, fn, { + quiet: true, + }); - const emit = emitter.emit.bind(emitter); - emit('test', message, fn); + const stream = supertape.createStream(); - const [output] = await Promise.all([ - pullout(stream, 'string'), - wait(emit, 'run'), + const [result] = await Promise.all([ + pull(stream), + once(emitter, 'end'), ]); - const result = output.slice(0, -1); - const expected = montag` TAP version 13 - # hello - ok 1 should be falsy - 1..1 - # tests 1 - # pass 1 + 1..0 + # tests 0 + # pass 0 # ok - `; t.equal(result, expected); - t.end(); -}); - -test('supertape: skip', async (t) => { - const fn = (t) => { - t.ok(true); - t.end(); - }; - const message = 'hello'; - const supertape = reRequire('..'); - - const emitter = supertape.skip(message, fn, { - quiet: true, - }); - - const [result] = await once(emitter, 'result'); - - t.notOk(result); t.end(); }); @@ -253,7 +196,10 @@ test('supertape: only', async (t) => { quiet: true, }); - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); const expected = montag` TAP version 13 @@ -265,7 +211,6 @@ test('supertape: only', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -292,7 +237,10 @@ test('supertape: extensions', async (t) => { extensions, }); - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); const expected = montag` TAP version 13 @@ -304,7 +252,6 @@ test('supertape: extensions', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -332,7 +279,10 @@ test('supertape: extensions: extend', async (t) => { quiet: true, }); - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); const expected = montag` TAP version 13 @@ -344,7 +294,6 @@ test('supertape: extensions: extend', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -372,7 +321,10 @@ test('supertape: extensions: extend: only', async (t) => { quiet: true, }); - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); const expected = montag` TAP version 13 @@ -384,7 +336,6 @@ test('supertape: extensions: extend: only', async (t) => { # pass 1 # ok - `; t.equal(result, expected); @@ -412,8 +363,81 @@ test('supertape: extensions: extend: skip', async (t) => { quiet: true, }); - const [result] = await once(emitter, 'result'); + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); - t.notOk(result, 'should skip'); + const expected = montag` + TAP version 13 + + 1..0 + # tests 0 + # pass 0 + + # ok + `; + + t.equal(result, expected); t.end(); }); + +test('supertape: quiet: false', async (t) => { + const extensions = { + transformCode: (t) => (a, b) => { + return t.equal(a + 1, b, 'should transform code'); + }, + }; + + const fn = (t) => { + t.transformCode(0, 1); + t.end(); + }; + + const message = 'hello'; + const {stdout} = process; + + Object.defineProperty(process, 'stdout', { + value: createStream(), + writable: false, + }); + + const supertape = reRequire('..'); + const extendedTape = supertape.extend(extensions); + + const emitter = extendedTape.skip(message, fn, { + quiet: false, + }); + + const [result] = await Promise.all([ + pull(supertape.createStream()), + once(emitter, 'end'), + ]); + + Object.defineProperty(process, 'stdout', { + value: stdout, + writable: false, + }); + + const expected = montag` + TAP version 13 + + 1..0 + # tests 0 + # pass 0 + + # ok + `; + + t.equal(result, expected); + t.end(); +}); + +function createStream() { + return new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk); + callback(); + }, + }); +}