diff --git a/cli.js b/cli.js index d72109484e..0ffdcad34a 100755 --- a/cli.js +++ b/cli.js @@ -23,6 +23,7 @@ var updateNotifier = require('update-notifier'); var chalk = require('chalk'); var Promise = require('bluebird'); var log = require('./lib/logger'); +var tap = require('./lib/tap'); var Api = require('./api'); // Bluebird specific @@ -36,7 +37,8 @@ var cli = meow([ ' --init Add AVA to your project', ' --fail-fast Stop after first test failure', ' --serial Run tests serially', - ' --require Module to preload (Can be repeated)', + ' --require Module to preload (Can be repeated)',, + ' --tap Generate TAP output', '', 'Examples', ' ava', @@ -54,7 +56,8 @@ var cli = meow([ ], boolean: [ 'fail-fast', - 'serial' + 'serial', + 'tap' ] }); @@ -65,7 +68,11 @@ if (cli.flags.init) { return; } -log.write(); +if (cli.flags.tap) { + console.log(tap.start()); +} else { + log.write(); +} var api = new Api(cli.input, { failFast: cli.flags.failFast, @@ -74,6 +81,11 @@ var api = new Api(cli.input, { }); api.on('test', function (test) { + if (cli.flags.tap) { + console.log(tap.test(test)); + return; + } + if (test.error) { log.error(test.title, chalk.red(test.error.message)); } else { @@ -87,17 +99,26 @@ api.on('test', function (test) { }); api.on('error', function (data) { + if (cli.flags.tap) { + console.log(tap.unhandledError(data)); + return; + } + log.unhandledError(data.type, data.file, data); }); api.run() .then(function () { - log.write(); - log.report(api.passCount, api.failCount, api.rejectionCount, api.exceptionCount); - log.write(); - - if (api.failCount > 0) { - log.errors(api.errors); + if (cli.flags.tap) { + console.log(tap.finish(api.passCount, api.failCount, api.rejectionCount, api.exceptionCount)); + } else { + log.write(); + log.report(api.passCount, api.failCount, api.rejectionCount, api.exceptionCount); + log.write(); + + if (api.failCount > 0) { + log.errors(api.errors); + } } process.stdout.write(''); diff --git a/lib/tap.js b/lib/tap.js new file mode 100644 index 0000000000..c4de30b8d9 --- /dev/null +++ b/lib/tap.js @@ -0,0 +1,67 @@ +'use strict'; +var format = require('util').format; + +// Parses stack trace and extracts original function name, file name and line. +function getSourceFromStack(stack, index) { + return stack + .split('\n') + .slice(index, index + 1) + .join('') + .replace(/^\s+at /, ''); +} + +exports.start = function () { + return 'TAP version 13'; +}; + +var i = 0; + +exports.test = function (test) { + var output; + + if (test.error) { + output = [ + '# ' + test.title, + format('not ok %d - %s', ++i, test.error.message), + ' ---', + ' operator: ' + test.error.operator, + ' expected: ' + test.error.expected, + ' actual: ' + test.error.actual, + ' at: ' + getSourceFromStack(test.error.stack, 3), + ' ...' + ]; + } else { + output = [ + '# ' + test.title, + format('ok %d - %s', ++i, test.title) + ]; + } + + return output.join('\n'); +}; + +exports.unhandledError = function (err) { + var output = [ + '# ' + err.message, + format('not ok %d - %s', ++i, err.message), + ' ---', + ' name: ' + err.name, + ' at: ' + getSourceFromStack(err.stack, 1), + ' ...' + ]; + + return output.join('\n'); +}; + +exports.finish = function (passCount, failCount, rejectionCount, exceptionCount) { + var output = [ + '', + '1..' + (passCount + failCount), + '# tests ' + (passCount + failCount), + '# pass ' + passCount, + '# fail ' + (failCount + rejectionCount + exceptionCount), + '' + ]; + + return output.join('\n'); +}; diff --git a/media/tap-output.png b/media/tap-output.png new file mode 100644 index 0000000000..deaa7875dd Binary files /dev/null and b/media/tap-output.png differ diff --git a/package.json b/package.json index 5f7163ba58..b89a850483 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,8 @@ "generators", "yield", "observable", - "observables" + "observables", + "tap" ], "dependencies": { "arr-flatten": "^1.0.1", diff --git a/readme.md b/readme.md index 2b33c37377..c54dd20a01 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,7 @@ Even though JavaScript is single-threaded, IO in Node.js can happen in parallel - [Async function support](#async-function-support) - [Observable support](#observable-support) - [Enhanced asserts](#enhanced-asserts) +- [Optional TAP output](#optional-tap-output) ## Test syntax @@ -105,6 +106,7 @@ $ ava --help --fail-fast Stop after first test failure --serial Run tests serially --require Module to preload (Can be repeated) + --tap Generate TAP output Examples ava @@ -456,6 +458,16 @@ test.cb(t => { }); ``` +### Optional TAP output + +AVA can generate TAP output via `--tap` option for use with any [TAP reporter](https://github.com/sindresorhus/awesome-tap#reporters). + +``` +$ ava --tap | tap-nyan +``` + +<img src="media/tap-output.png" width="398"> + ## API diff --git a/test/tap.js b/test/tap.js new file mode 100644 index 0000000000..577bc981f6 --- /dev/null +++ b/test/tap.js @@ -0,0 +1,89 @@ +'use strict'; +var test = require('tap').test; +var tap = require('../lib/tap'); + +test('start', function (t) { + t.is(tap.start(), 'TAP version 13'); + t.end(); +}); + +test('passing test', function (t) { + var actualOutput = tap.test({ + title: 'passing' + }); + + var expectedOutput = [ + '# passing', + 'ok 1 - passing' + ].join('\n'); + + t.is(actualOutput, expectedOutput); + t.end(); +}); + +test('failing test', function (t) { + var actualOutput = tap.test({ + title: 'failing', + error: { + message: 'false == true', + operator: '==', + expected: true, + actual: false, + stack: ['', '', '', ' at Test.fn (test.js:1:2)'].join('\n') + } + }); + + var expectedOutput = [ + '# failing', + 'not ok 2 - false == true', + ' ---', + ' operator: ==', + ' expected: true', + ' actual: false', + ' at: Test.fn (test.js:1:2)', + ' ...' + ].join('\n'); + + t.is(actualOutput, expectedOutput); + t.end(); +}); + +test('unhandled error', function (t) { + var actualOutput = tap.unhandledError({ + message: 'unhandled', + name: 'TypeError', + stack: ['', ' at Test.fn (test.js:1:2)'].join('\n') + }); + + var expectedOutput = [ + '# unhandled', + 'not ok 3 - unhandled', + ' ---', + ' name: TypeError', + ' at: Test.fn (test.js:1:2)', + ' ...' + ].join('\n'); + + t.is(actualOutput, expectedOutput); + t.end(); +}); + +test('results', function (t) { + var passCount = 1; + var failCount = 2; + var rejectionCount = 3; + var exceptionCount = 4; + + var actualOutput = tap.finish(passCount, failCount, rejectionCount, exceptionCount); + var expectedOutput = [ + '', + '1..' + (passCount + failCount), + '# tests ' + (passCount + failCount), + '# pass ' + passCount, + '# fail ' + (failCount + rejectionCount + exceptionCount), + '' + ].join('\n'); + + t.is(actualOutput, expectedOutput); + t.end(); +});