diff --git a/Makefile b/Makefile index 72cc0be..631b008 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: pass fail test +.PHONY: pass fail mix test TAP = ./node_modules/tape/bin/tape -MIN = ./bin/tap-min +MIN = ./bin/tap-difflet pass: @$(TAP) test/pass.js | $(MIN) @@ -9,4 +9,10 @@ pass: fail: @$(TAP) test/fail.js | $(MIN) +mix: + @$(TAP) test/mix.js | $(MIN) + +mixp: + @$(TAP) test/mix.js | $(MIN) -p + test: pass fail diff --git a/README.md b/README.md index 8baff68..a460feb 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,38 @@ -# tap-min +# tap-difflet -Minimal TAP output formatter. +TAP output formatter using difflet for inequality errors. ## Installation ~~~ text -npm install -g tap-min -npm install tap-min --save-dev +npm install -g tap-difflet +npm install tap-difflet --save-dev ~~~ ## Usage ~~~ text -tape test/*.js | tap-min -node test.js | ./node_modules/tap-min/bin/tap-min +tape test/*.js | tap-difflet [options] + +Options: + -p --pessimistic Only output failed tests. + -v --version Print the version of tap-difflet. + -h --help Show this. ~~~ -## Output +It also can be used inside node code + +~~~ text +var tapDifflet = require('tap-difflet'); -![tap-min](http://i.imgur.com/x7G4thJ.png) +var formatter = tapDifflet({ + pessimistic: true // Only output failed tests. `false` by default. +}); +~~~ + +## Output -![tap-min](http://i.imgur.com/dgII7bF.png) +![tap-difflet](http://i.imgur.com/8uFAvXU.png) ## License @@ -28,6 +40,8 @@ The MIT License (MIT) Copyright (c) 2014 Ellen Gummesson +Copyright (c) 2014 Louis Acresti + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/bin/tap-difflet b/bin/tap-difflet new file mode 100755 index 0000000..85279a0 --- /dev/null +++ b/bin/tap-difflet @@ -0,0 +1,36 @@ +#!/usr/bin/env node + + +var docopt = require('docopt').docopt; + +var usage = [ + 'Process TAP output on STDIN and print diffs of failed comparisons.', + '', + 'Usage: tap-difflet [options]', + '', + 'Options:', + ' -p --pessimistic Only output failed tests.', + ' -v --version Print the version of tap-difflet.', + ' -h --help Show this.', +].join('\n'); + +var pkg = require('../package.json'); +var options = docopt(usage, { + version: pkg.version, +}); + +function getOptionsWithoutDashes(options) { + const transformedOptions = {} + Object.keys(options).forEach(function(key) { + const keyWithoutDashes = key.replace(/^(--|-)/, ''); + transformedOptions[keyWithoutDashes] = options[key]; + }); + return transformedOptions; +} + +var formatter = require('..')(getOptionsWithoutDashes(options)); + +process.stdin.pipe(formatter).pipe(process.stdout); +process.on('exit', function () { + process.exit(formatter._errors.length); +}); diff --git a/bin/tap-min b/bin/tap-min deleted file mode 100755 index e0c13d4..0000000 --- a/bin/tap-min +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node - -/* Modules */ - -var parser = require('tap-parser'); -var through = require('through2'); -var duplexer = require('duplexer'); -var hirestime = require('hirestime'); -var prettyms = require('pretty-ms'); -var chalk = require('chalk'); - -var tap = parser(); -var out = through(); -var dup = duplexer(tap, out); - -/* Helpers */ - -function output(str) { - out.push(' ' + str); - out.push('\n'); -} - -function format(total, time) { - var word = (total > 1) ? 'tests' : 'test'; - return total + ' ' + word + ' complete (' + time + ')'; -} - -/* Parser */ - -var timer = hirestime(); -var errors = []; -var current = null; - -tap.on('comment', function(res) { - current = '\n' + ' ' + res; -}); - -tap.on('assert', function(res) { - var assert = current + ' ' + res.name; - if (!res.ok) { errors.push(chalk.white(assert)); } -}); - -tap.on('extra', function(res) { - if (res !== '') { errors.push(chalk.gray(res)); } -}); - -tap.on('results', function(res) { - var count = res.asserts.length; - var time = prettyms(timer()); - out.push('\n'); - if (errors.length) { - output(chalk.red(format(count, time))); - errors.forEach(function(error) { - output(error); - }); - } else { - output(chalk.green(format(count, time))); - } - out.push('\n'); -}); - -/* Output */ - -process.stdin.pipe(dup).pipe(process.stdout); - -// vim:filetype=javascript diff --git a/index.js b/index.js new file mode 100644 index 0000000..820755c --- /dev/null +++ b/index.js @@ -0,0 +1,197 @@ +var parser = require('tap-parser-yaml'); +var through = require('through2'); +var duplexer = require('duplexer'); +var hirestime = require('hirestime'); +var prettyms = require('pretty-ms'); +var chalk = require('chalk'); +var objdiff = require('difflet')({ + indent: 2, + comment: true +}).compare; +var ansidiff = require('ansidiff'); + + +module.exports = function (options) { + if (!options) options = {}; + + var tap = parser(); + var out = through(); + var dup = duplexer(tap, out); + + if (options.pessimistic) { + var anAssertFailed = true; + var originalOut = out; + var buffer = null; + tap.on('comment', function (comment) { + if (buffer && anAssertFailed) { + for (var bi = 0; bi < buffer.length; ++bi) { + originalOut.push(buffer[bi]); + } + } + + buffer = null; + anAssertFailed = false; + out = originalOut; + + if (getCommentType(comment) === CommentType.OTHER) { + buffer = []; + out = buffer; + } + }); + + tap.on('assert', function (res) { + if (!res.ok) { + anAssertFailed = true; + } + }); + } + + /* Helpers */ + var CommentType = { + OTHER: 1, + TESTS: 2, + PASS: 3, + FAIL: 4, + OK: 5 + }; + + function getCommentType(comment) { + if (/^tests\s+[0-9]+$/gi.test(comment)) { + return CommentType.TESTS; + } + else if (/^pass\s+[0-9]+$/gi.test(comment)) { + return CommentType.PASS; + } + else if (/^fail\s+[0-9]+$/gi.test(comment)) { + return CommentType.FAIL; + } + else if (/^ok$/gi.test(comment)) { + return CommentType.OK; + } + else { + return CommentType.OTHER; + } + }; + + function output(str) { + out.push(str); + } + + /* Parser */ + + var timer = hirestime(); + var errors = []; + + tap.on('comment', function (comment) { + var commentType = getCommentType(comment); + if (commentType === CommentType.TESTS + || commentType === CommentType.OK + || commentType === CommentType.PASS + || commentType === CommentType.FAIL) { + return; + } + + output('\n ' + chalk.white.bold(comment) + '\n'); + }); + + tap.on('assert', function (res) { + if (res.ok) { + output(' ' + chalk.green.bold('✓') + ' ' + chalk.gray(res.name) + '\n'); + } else { + assert = chalk.red.bold('⨯ ' + res.name + '\n'); + errors.push(chalk.white(assert)); + output(' ' + errors[errors.length - 1]); + } + }); + + tap.on('extra', function (res) { + if (res !== '') { + if (res.indexOf('---') === 0) { + errors.push(chalk.gray(res)); + output(errors[errors.length - 1]); + output('\n'); + } else { + output(' ' + chalk.blue(res)); + output('\n'); + } + } + }); + + tap.on('results', function (res) { + var count = res.asserts.length; + var time = prettyms(timer()); + output('\n'); + + if (errors.length) { + output(chalk.red.bold(errors.length + ' failing\n')); + } + output(chalk.green.bold(Math.max(0, count - errors.length) + ' passing') + chalk.gray(' (' + time + ')')); + + output('\n\n'); + }); + + tap.on('diag', function (diag) { + var expected, actual, at, + gotExpected = true, + gotActual = true, + gotAt = true, + str = ''; + + if (diag.hasOwnProperty('expected')) { + expected = diag.expected; + } else if (diag.hasOwnProperty('wanted')) { + expected = diag.wanted; + } else { + gotExpected = false; + } + + if (diag.hasOwnProperty('actual')) { + actual = diag.actual; + } else if (diag.hasOwnProperty('found')) { + actual = diag.found; + } else { + gotActual = false; + } + + if (diag.hasOwnProperty('at')) { + at = diag.at; + } + else { + gotAt = false; + } + + if (gotActual && gotExpected) { + if (typeof expected !== typeof actual || + typeof expected === 'object' && (!actual || !expected)) { + str = 'Expected ' + typeof expected + ' but got ' + typeof actual; + } else if (typeof expected === 'string') { + if (str.indexOf('\n') >= 0) { + str = ansidiff.lines(expected, actual); + } else { + str = ansidiff.chars(expected, actual); + } + } else if (typeof expected === 'object') { + str = objdiff(expected, actual); + } else { + str = chalk.white('Expected ') + chalk.bold('' + expected) + chalk.white(' but got ') + chalk.bold('' + actual); + } + } + + if (gotAt) { + str = str + '\n\n' + chalk.grey('At: ' + at); + } + + if (str) { + str = '\n' + str; + str = str.replace(/\n/g, '\n '); + errors[errors.length - 1] += str; + output(str); + output('\n'); + } + + }); + + dup._errors = errors; + + return dup; +}; diff --git a/package.json b/package.json index 1cc2671..a9db0c9 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,47 @@ { - "name": "tap-min", - "version": "0.1.0", - "description": "Minimal TAP output formatter.", + "name": "tap-difflet", + "version": "0.7.2", + "description": "TAP output formatter using difflet for inequality errors.", "bin": { - "tap-min": "bin/tap-min" + "tap-difflet": "bin/tap-difflet" }, "scripts": { "test": "make test" }, "repository": { "type": "git", - "url": "https://github.com/gummesson/tap-min.git" + "url": "https://github.com/namuol/tap-difflet.git" }, "keywords": [ "test", "tap", "tape", "mocha", + "difflet", + "diff", "reporter" ], "author": "Ellen Gummesson", + "contributors": [ + "Louis Acresti (http://namuol.github.io/)" + ], "license": "MIT", "bugs": { - "url": "https://github.com/gummesson/tap-min/issues" + "url": "https://github.com/namuol/tap-difflet/issues" }, - "homepage": "https://github.com/gummesson/tap-min", + "homepage": "https://github.com/namuol/tap-difflet", "devDependencies": { - "tape": "^2.12.1" + "tape": "^3.0.0" }, "dependencies": { - "tap-parser": "^0.4.3", - "through2": "^0.4.1", + "ansidiff": "^1.0.0", + "chalk": "^2.3.2", + "difflet": "^0.2.6", + "docopt": "^0.6.2", "duplexer": "^0.1.1", "hirestime": "^0.2.4", - "pretty-ms": "^0.1.0", - "chalk": "^0.4.0" + "pretty-ms": "^1.0.0", + "tap-parser-yaml": "^0.7.2", + "through2": "^0.6.2" } } diff --git a/test/fail.js b/test/fail.js index 94fd998..dca3053 100644 --- a/test/fail.js +++ b/test/fail.js @@ -6,10 +6,28 @@ var test = require('tape'); test('1 === 2', function(assert) { assert.plan(1); - assert.equal(1, 2); + setTimeout(function () { + assert.equal(1, 2); + }, 500); }); test('2 === 1', function(assert) { assert.plan(1); - assert.equal(2, 1); + setTimeout(function () { + assert.equal(2, 1); + }, 500); }); + +test('{a: 404} === {a: 42}', function(assert) { + assert.plan(1); + setTimeout(function () { + assert.deepEqual({a: 404}, {a: 42}); + }, 500); +}); + +test('"Hell, world!" === "Hello, world!"', function(assert) { + assert.plan(1); + setTimeout(function () { + assert.equal("Hell, world!", "Hello, world!"); + }, 500); +}) diff --git a/test/mix.js b/test/mix.js new file mode 100644 index 0000000..4f7a31a --- /dev/null +++ b/test/mix.js @@ -0,0 +1,2 @@ +require ('./pass'); +require ('./fail'); \ No newline at end of file diff --git a/test/pass.js b/test/pass.js index a4d0808..c4ac5d9 100644 --- a/test/pass.js +++ b/test/pass.js @@ -13,3 +13,9 @@ test('2 === 2', function(assert) { assert.plan(1); assert.equal(2, 2); }); + +test('Does not error with comment', function(assert){ + assert.plan(1); + console.log('a comment'); + assert.pass('can console.log'); +})