From 0bdad7fd501b723852383994eacd441d018c2bb5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 13:46:12 -0700 Subject: [PATCH 1/7] lib/suite: Add _beforeEach, _beforeAll, _afterEach, and _afterAll Catching up with: * Mocha, which has had these as part of its suites since mochajs/mocha@c0bb9188 (plugging in Hook, 2011-11-25), and * Mochawesome, which has expected arrays since at least adamgruber/mochawesome@5a49d410 (3.0, 2017-11-30, adamgruber/mochawesome#214). Without this change, Mochawesome's: [].concat(suite._beforeAll, suite._beforeEach) will lead to: [undefined, undefined] and Mochawesome will call cleanTest on those undefined values and crash. --- lib/suite.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/suite.js b/lib/suite.js index d1ed52c..ba1217f 100644 --- a/lib/suite.js +++ b/lib/suite.js @@ -12,6 +12,10 @@ function Suite (parent) { this.suites = [] this.tests = [] this.ok = true + this._beforeEach = [] + this._beforeAll = [] + this._afterEach = [] + this._afterAll = [] } Suite.prototype.fullTitle = function () { From 46b55257c534cc0f02d382c614ba8322f0f0e183 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 15:04:13 -0700 Subject: [PATCH 2/7] index.js: Call reporter.done (if it exists) Catching up with mochajs/mocha@30582e64 (if a reporter has a .done method, call it before exiting, 2014-05-17, mochajs/mocha#1218). --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 46c5663..32a45d3 100755 --- a/index.js +++ b/index.js @@ -50,12 +50,16 @@ function Formatter (type, options) { } var runner = this.runner = new Runner(options) - this.reporter = new reporters[type](this.runner, {}) + var reporter = this.reporter = new reporters[type](this.runner, {}) Writable.call(this, options) runner.on('end', function () { if (!runner.parser.ok) exitCode = 1 + + if (reporter.done) { + reporter.done(runner.stats.failures, process.exit) + } }) } From ef42cbe11194f07d13e2bc9f4962e1e46301af81 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 15:48:07 -0700 Subject: [PATCH 3/7] lib/test: Support skipped tests for Mochawesome Mochawesome has used: skipped = (!cleaned.pass && !cleaned.fail && !cleaned.pending); since adamgruber/mochawesome@2d4a63b7 (logic to record skipped tests, 2015-01-29). Do not emit 'test end' for skips or TODO (pending) tests, because Mochawesome has: obj.stats.skipped = obj.stats.testsRegistered - obj.stats.tests; since that same commit, so we need these tests entered in testsRegistered (via the 'test' event) but not in tests (via the 'test end' event). Do not emit 'pass' for skips either, otherwise we'll end up with failures + passes > tests. Also fix "pass" -> "passed" for .state. Mochawesome has been comparing the value to "passed" in its cleaner since adamgruber/mochawesome@d687633d (Initial commit, 2014-07-11), and Mocha itself has been comparing with "passed" since at least mochajs/mocha@2c720a35 (do not eat exceptions thrown asynchronously from passed tests, 2018-02-28, mochajs/mocha#3257). --- lib/runner.js | 13 ++++++++----- lib/test.js | 8 ++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 4b5ea0f..d645d42 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -39,7 +39,8 @@ module.exports = Runner // // test end(test) // Emitted immediately after the "test" event because test points are -// not async in TAP. +// not async in TAP. This event is only emitted for passes and +// failures; it is not emitted for skips or TODO tests. var util = require('util') var Test = require('./test.js') @@ -281,15 +282,17 @@ function emitTest (parser, result) { } runner.emit('test', test) - if (result.skip || result.todo) { + if (test.pending) { runner.emit('pending', test) - } else if (result.ok) { + } else if (test.state === 'passed') { runner.emit('pass', test) - } else { + } else if (test.state === 'failed') { var error = getError(result) runner.emit('fail', test, error) } - runner.emit('test end', test) + if (!test.pending && !result.skip) { + runner.emit('test end', test) + } } function getError (result) { diff --git a/lib/test.js b/lib/test.js index a641b14..cf6ec44 100644 --- a/lib/test.js +++ b/lib/test.js @@ -7,8 +7,12 @@ function Test (result, parent) { this._slow = 75 this.duration = result.time this.title = result.name - this.state = result.ok ? 'pass' : 'failed' - this.pending = result.todo || result.skip || false + this.pending = result.todo || false + if (result.ok) { + this.state = result.skip ? 'skipped' : 'passed' + } else { + this.state = 'failed' + } if (result.diag && result.diag.source) { var source = result.diag.source this.fn = { From 7d50e22f0ea5af851489f18d5013430bceff05e8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 13:51:30 -0700 Subject: [PATCH 4/7] lib/runner: Attach a suite to the runner Mochawesome has expected a suite attached to the runner since adamgruber/mochawesome@e25a5eb0 (handle nested describes, 2014-07-14). This commit creates a new root Suite for that, and attaches the per-TAP suites as children of the root suite. This will allow us to render output from multiple TAP files with Mochawesome. I've also refactored the ancestory properties: * Test.parent became Test.suite, since that's the only object type we're using there. * Suite.parent now only references other Suites. There's no need to complicate the ancestry by including both Suites and Parsers, and sticking to suites means we don't need Parser methods for title chaining. The new titlePath methods catch us up with mochajs/mocha@aa24e82f (indent test contexts, 2017-05-20, mochajs/mocha#2814). I've left fullTitle around for now for compat with older versions of Mocha. --- lib/runner.js | 32 ++++++++++---------------------- lib/suite.js | 33 ++++++++++++++++++++++----------- lib/test.js | 24 ++++++++++++++++++++---- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index d645d42..86e855f 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -77,6 +77,9 @@ Runner.prototype.write = function () { if (!this.emittedStart) { this.emittedStart = true this.emit('start') + this.suite = new Suite('root') + this.parser.parent = this + this.emit('suite', this.suite) } return this.parser.write.apply(this.parser, arguments) @@ -86,13 +89,6 @@ Runner.prototype.end = function () { return this.parser.end.apply(this.parser, arguments) } -Parser.prototype.fullTitle = function () { - if (!this.parent) - return this.name || '' - else - return (this.parent.fullTitle() + ' ' + (this.name || '')).trim() -} - function attachEvents (runner, parser, level) { parser.runner = runner @@ -104,6 +100,7 @@ function attachEvents (runner, parser, level) { runner.emit('version', v) }) parser.on('complete', function (res) { + runner.emit('suite end', runner.suite) runner.emit('end') }) parser.on('comment', function (c) { @@ -257,9 +254,11 @@ function attachEvents (runner, parser, level) { function emitSuite (parser) { if (!parser.emittedSuite && parser.name) { parser.emittedSuite = true - var suite = parser.suite = new Suite(parser) - if (parser.parent && parser.parent.suite) - parser.parent.suite.suites.push(suite) + var ancestor = parser + while (ancestor && !ancestor.suite) + ancestor = ancestor.parent + var suite = parser.suite = new Suite(parser.name, ancestor.suite) + if (parser.runner.stats) parser.runner.stats.suites ++ @@ -269,18 +268,7 @@ function emitSuite (parser) { function emitTest (parser, result) { var runner = parser.runner - var test = new Test(result, parser) - - if (parser.suite) { - parser.suite.tests.push(test) - if (!result.ok) { - for (var p = parser; p && p.suite; p = p.parent) { - p.suite.ok = false - } - } - parser.suite.ok = parser.suite.ok && result.ok - } - + var test = new Test(result, parser.suite) runner.emit('test', test) if (test.pending) { runner.emit('pending', test) diff --git a/lib/suite.js b/lib/suite.js index ba1217f..d8effc9 100644 --- a/lib/suite.js +++ b/lib/suite.js @@ -2,13 +2,9 @@ module.exports = Suite -function Suite (parent) { - if (!parent.parent || !parent.parent.emittedSuite) - this.root = true - else - this.root = false - - this.title = parent.name || '' +function Suite (title, parent) { + this.root = !parent + this.title = title this.suites = [] this.tests = [] this.ok = true @@ -16,11 +12,26 @@ function Suite (parent) { this._beforeAll = [] this._afterEach = [] this._afterAll = [] + + Object.defineProperty(this, 'parent', { + value: parent, + writable: true, + configurable: true, + enumerable: false + }) + + if (parent) { + parent.suites.push(this) + } } Suite.prototype.fullTitle = function () { - if (!this.parent) - return (this.title || '').trim() - else - return (this.parent.fullTitle() + ' ' + (this.title || '')).trim() + return this.titlePath().join(' ').trim() +} + +Suite.prototype.titlePath = function () { + var title = [(this.title || '').trim()] + if (this.parent && this.parent.titlePath) + return this.parent.titlePath().concat(title) + return title } diff --git a/lib/test.js b/lib/test.js index cf6ec44..694285f 100644 --- a/lib/test.js +++ b/lib/test.js @@ -2,7 +2,7 @@ module.exports = Test -function Test (result, parent) { +function Test (result, suite) { this.result = result this._slow = 75 this.duration = result.time @@ -22,16 +22,32 @@ function Test (result, parent) { } } - Object.defineProperty(this, 'parent', { - value: parent, + Object.defineProperty(this, 'suite', { + value: suite, writable: true, configurable: true, enumerable: false }) + + if (suite) { + suite.tests.push(this) + if (!result.ok) { + for (var ancestor = suite; ancestor; ancestor = ancestor.parent) { + ancestor.ok = false + } + } + } } Test.prototype.fullTitle = function () { - return (this.parent.fullTitle() + ' ' + (this.title || '')).trim() + return this.titlePath().join(' ').trim() +} + +Test.prototype.titlePath = function () { + var title = [(this.title || '').trim()] + if (this.suite && this.suite.titlePath) + return this.suite.titlePath().concat(title) + return title } Test.prototype.slow = function (ms){ From 8b73357dc7f74f8e7a923fe0f525710e8cefa61c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 12:47:26 -0700 Subject: [PATCH 5/7] index.js: Fallback to require(...) for unrecognized reporters This allows us to use reporters that aren't built in. For example, with this commit you can: $ npm install $ npm install --no-save mocha mochawesome $ cp index.js node_modules/tap-mocha-reporter/ $ cp lib/*.js node_modules/tap-mocha-reporter/lib/ $ npm test -- --reporter=mochawesome That's installing mocha@5.2.0 and mochawesome@3.0.2. --- index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 32a45d3..5bd80dc 100755 --- a/index.js +++ b/index.js @@ -25,7 +25,15 @@ function Formatter (type, options) { if (!(this instanceof Formatter)) { return new Formatter(type, options) } - if (!reporters[type]) { + var _reporter = reporters[type]; + if (!_reporter) { + try { + _reporter = require(type); + } catch (err) { + console.warn(err); + } + } + if (!_reporter) { console.error('Unknown format type: %s\n\n%s', type, avail()) type = 'silent' } @@ -50,7 +58,7 @@ function Formatter (type, options) { } var runner = this.runner = new Runner(options) - var reporter = this.reporter = new reporters[type](this.runner, {}) + var reporter = this.reporter = new _reporter(this.runner, {}) Writable.call(this, options) runner.on('end', function () { From aca41744600b5a7c1a76145a8130c6af07b066b9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 16:30:43 -0700 Subject: [PATCH 6/7] lib/test: Add diagnostic Test.context for Mochawesome This is, as far as I know, Mochawesome-specific, which makes it a bit of a hack. The Mochawesome side of this is adamgruber/mochawesome@65e90621 (implement addContext method for adding report context to tests, 2016-12-20, adamgruber/mochawesome#106). Ideally we'd want a way to trigger this as part of requesting Mochawesome as the reporter, but the addition seems harmless enough so I'm just including it every time. In the diag.source case, we might want to expose diagnostics besides .fn. But we surely don't want to expose the test source via both .fn and .context. For this commit, I've left the diag.source case alone, but in future work we might want something closer to : if (result.diag) { var context = Object.assign({}, result.diag) if (context.source) { var source = context.source delete context.source this.fn = { toString: function () { return 'function(){' + source + '\n}' } } } if (Object.keys(context).length) { this.context = { title: 'diagnostic', value: context, } } } I haven't done that here, because Object.assign is not supported on Internet Exporer, Android webview, or Opera for Android [1], despite being part of ECMAScript 2015 [2]. Object.keys seems to be supported everywhere [3]. We may only care about Node for this package, but I'm being conservative for this commit. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Browser_compatibility [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Specifications [3]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys#Browser_compatibility --- lib/test.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/test.js b/lib/test.js index 694285f..97dcd4b 100644 --- a/lib/test.js +++ b/lib/test.js @@ -13,11 +13,18 @@ function Test (result, suite) { } else { this.state = 'failed' } - if (result.diag && result.diag.source) { - var source = result.diag.source - this.fn = { - toString: function () { - return 'function(){' + source + '\n}' + if (result.diag) { + if (result.diag.source) { + var source = result.diag.source + this.fn = { + toString: function () { + return 'function(){' + source + '\n}' + } + } + } else { + this.context = { + title: 'diagnostic', + value: result.diag, } } } From e4f818396e15addaf3c26640382eaa1d664930f1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 7 Jun 2018 20:54:12 -0700 Subject: [PATCH 7/7] lib/test: Fall back to the skip message for test titles Skips like [1]: ok 23 # skip Insufficient flogiston pressure. are parsed (at least by tap-parser@1.2.2) into structures like: { id: 1, name: "", ok: true, skip: "root.readonly is false but the root filesystem is still not writable" } But title-less tests are not very useful. With this change, cases like the above empty-string name will fall back to the skip value. And if that skip value is undefined, we'll fall back to an empty string (because I'm not sure how well downstream consumers would handle and undefined title). One positive effect of this change is that Mochawesome now has a title message to render for these skips (where previously it just used an empty h4). [1]: https://testanything.org/tap-version-13-specification.html#skipping-tests --- lib/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/test.js b/lib/test.js index 97dcd4b..a5e0f56 100644 --- a/lib/test.js +++ b/lib/test.js @@ -6,7 +6,7 @@ function Test (result, suite) { this.result = result this._slow = 75 this.duration = result.time - this.title = result.name + this.title = result.name || result.skip || '' this.pending = result.todo || false if (result.ok) { this.state = result.skip ? 'skipped' : 'passed'