diff --git a/.gitignore b/.gitignore index 2621831c..0174f4b3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ coverage/ !test/fixtures/custom-framework-app/node_modules/ !test/fixtures/demo-app/node_modules/aliyun-egg/node_modules/ !test/fixtures/test-files-glob/** +!test/fixtures/test-files-stack/node_modules/ .tmp .vscode *.log diff --git a/README.md b/README.md index c8a1cd77..b2bdb887 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ You can pass any mocha argv. - `--require` require the given module - `--grep` only run tests matching - `--timeout` milliseconds, default to 30000 +- `--full-trace` display the full stack trace, default to false. - see more at https://mochajs.org/#usage #### environment diff --git a/lib/cmd/test.js b/lib/cmd/test.js index 8b15ef43..f1107bdb 100644 --- a/lib/cmd/test.js +++ b/lib/cmd/test.js @@ -26,6 +26,9 @@ class TestCommand extends Command { alias: 't', type: 'number', }, + 'full-trace': { + description: 'display the full stack trace', + }, }; } @@ -69,6 +72,10 @@ class TestCommand extends Command { /* istanbul ignore next */ if (!Array.isArray(requireArr)) requireArr = [ requireArr ]; + // clean mocha stack, inspired by https://github.com/rstacruz/mocha-clean + // [mocha built-in](https://github.com/mochajs/mocha/blob/master/lib/utils.js#L738) don't work with `[npminstall](https://github.com/cnpm/npminstall)`, so we will override it. + if (!testArgv.fullTrace) requireArr.unshift(require.resolve('../mocha-clean')); + requireArr.push(require.resolve('co-mocha')); if (requireArr.includes('intelli-espower-loader')) { diff --git a/lib/mocha-clean.js b/lib/mocha-clean.js new file mode 100644 index 00000000..08ea8e16 --- /dev/null +++ b/lib/mocha-clean.js @@ -0,0 +1,39 @@ +'use strict'; + +const mocha = require('mocha'); +const internal = [ + '(timers.js:', + '(node.js:', + '(module.js:', + '(domain.js:', + 'GeneratorFunctionPrototype.next (native)', + 'at Generator.next', + // node 8.x + 'at Promise ()', + 'at next (native)', + '__mocha_internal__', + /node_modules\/.*empower-core\//, + /node_modules\/.*mocha\//, + /node_modules\/.*co\//, + /node_modules\/.*co-mocha\//, +]; + +// monkey-patch `Runner#fail` to modify stack +const originFn = mocha.Runner.prototype.fail; +mocha.Runner.prototype.fail = function(test, err) { + /* istanbul ignore else */ + if (err.stack) { + const stack = err.stack.split('\n').filter(line => { + line = line.replace(/\\\\?/g, '/'); + return !internal.some(rule => match(line, rule)); + }); + stack.push(' [use `--full-trace` to display the full stack trace]'); + err.stack = stack.join('\n'); + } + return originFn.call(this, test, err); +}; + +function match(line, rule) { + if (rule instanceof RegExp) return rule.test(line); + return line.includes(rule); +} diff --git a/package.json b/package.json index bc2cf8ea..2daef457 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ "co-mocha": "^1.2.0", "common-bin": "^2.4.0", "debug": "^2.6.8", - "detect-port": "^1.1.3", + "detect-port": "^1.2.0", "egg-utils": "^2.2.0", "globby": "^6.1.0", "intelli-espower-loader": "^1.0.1", "istanbul": "^1.1.0-alpha.1", "mocha": "^3.4.2", "mz-modules": "^1.0.0", - "power-assert": "^1.4.2", + "power-assert": "^1.4.3", "ypkgfiles": "^1.4.0" }, "devDependencies": { diff --git a/test/fixtures/test-files-stack/node_modules/my-sleep/index.js b/test/fixtures/test-files-stack/node_modules/my-sleep/index.js new file mode 100644 index 00000000..5082d237 --- /dev/null +++ b/test/fixtures/test-files-stack/node_modules/my-sleep/index.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = fn => { + setTimeout(() => { + fn(); + }, 10); +} \ No newline at end of file diff --git a/test/fixtures/test-files-stack/package.json b/test/fixtures/test-files-stack/package.json new file mode 100644 index 00000000..36e80c69 --- /dev/null +++ b/test/fixtures/test-files-stack/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-files-stack" +} \ No newline at end of file diff --git a/test/fixtures/test-files-stack/test/assert.test.js b/test/fixtures/test-files-stack/test/assert.test.js new file mode 100644 index 00000000..be497c49 --- /dev/null +++ b/test/fixtures/test-files-stack/test/assert.test.js @@ -0,0 +1,11 @@ +'use strict'; + +const assert = require('assert'); + +describe('assert.test.js', () => { + it('should fail with simplify stack', () => { + [ 1 ].forEach(() => { + assert(1 === 0); + }); + }); +}); diff --git a/test/fixtures/test-files-stack/test/promise.test.js b/test/fixtures/test-files-stack/test/promise.test.js new file mode 100644 index 00000000..b29a068b --- /dev/null +++ b/test/fixtures/test-files-stack/test/promise.test.js @@ -0,0 +1,13 @@ +'use strict'; + +const co = require('co'); + +describe('promise.test.js', () => { + it('should fail with simplify stack', function* () { + yield co(function* () { + return yield new Promise((resolve, reject) => { + reject(new Error('this is an error')); + }); + }); + }); +}); diff --git a/test/fixtures/test-files-stack/test/sleep.test.js b/test/fixtures/test-files-stack/test/sleep.test.js new file mode 100644 index 00000000..b9e403b7 --- /dev/null +++ b/test/fixtures/test-files-stack/test/sleep.test.js @@ -0,0 +1,11 @@ +'use strict'; + +const sleep = require('my-sleep'); + +describe('sleep.test.js', () => { + it('should fail with simplify stack', done => { + sleep(() => { + done(new Error('this is an error')); + }); + }); +}); diff --git a/test/lib/cmd/cov.test.js b/test/lib/cmd/cov.test.js index 6aa3f6bb..763a31f2 100644 --- a/test/lib/cmd/cov.test.js +++ b/test/lib/cmd/cov.test.js @@ -136,7 +136,7 @@ describe('test/lib/cmd/cov.test.js', () => { mm(process.env, 'TESTS', 'noexist.js'); const cwd = path.join(__dirname, '../../fixtures/prerequire'); yield coffee.fork(eggBin, [ 'cov' ], { cwd }) - .debug() + // .debug() .coverage(false) .expect('code', 0) .end(); diff --git a/test/lib/cmd/test.test.js b/test/lib/cmd/test.test.js index 45309cb6..0d8a33c0 100644 --- a/test/lib/cmd/test.test.js +++ b/test/lib/cmd/test.test.js @@ -3,6 +3,7 @@ const path = require('path'); const coffee = require('coffee'); const mm = require('mm'); +const assert = require('assert'); describe('test/lib/cmd/test.test.js', () => { const eggBin = require.resolve('../../../bin/egg-bin.js'); @@ -107,4 +108,63 @@ describe('test/lib/cmd/test.test.js', () => { .expect('code', 0) .end(); }); + + describe('simplify mocha error stack', () => { + const cwd = path.join(__dirname, '../../fixtures/test-files-stack'); + + it('should clean assert error stack', done => { + mm(process.env, 'TESTS', 'test/assert.test.js'); + coffee.fork(eggBin, [ 'test' ], { cwd }) + // .debug() + .end((err, { stdout, code }) => { + assert(stdout.match(/AssertionError/)); + assert(stdout.match(/at forEach .*assert.test.js:\d+:\d+/)); + assert(stdout.match(/at Context.it .*assert.test.js:\d+:\d+/)); + assert(stdout.match(/\bat\s+/g).length === 3); + assert(code === 1); + done(err); + }); + }); + + it('should should show full stack trace', done => { + mm(process.env, 'TESTS', 'test/assert.test.js'); + coffee.fork(eggBin, [ 'test', '--full-trace' ], { cwd }) + // .debug() + .end((err, { stdout, code }) => { + assert(stdout.match(/AssertionError/)); + assert(stdout.match(/at .*node_modules.*mocha/)); + assert(stdout.match(/\bat\s+/g).length > 10); + assert(code === 1); + done(err); + }); + }); + + it('should clean co error stack', done => { + mm(process.env, 'TESTS', 'test/promise.test.js'); + coffee.fork(eggBin, [ 'test' ], { cwd }) + // .debug() + .end((err, { stdout, code }) => { + assert(stdout.match(/Error: this is an error/)); + assert(stdout.match(/at Promise .*promise.test.js:\d+:\d+/)); + assert(stdout.match(/at Context\. .*promise.test.js:\d+:\d+/)); + assert(stdout.match(/\bat\s+/g).length === 3); + assert(code === 1); + done(err); + }); + }); + + it('should clean callback error stack', done => { + mm(process.env, 'TESTS', 'test/sleep.test.js'); + coffee.fork(eggBin, [ 'test' ], { cwd }) + // .debug() + .end((err, { stdout, code }) => { + assert(stdout.match(/Error: this is an error/)); + assert(stdout.match(/at sleep .*sleep.test.js:\d+:\d+/)); + assert(stdout.match(/at Timeout.setTimeout .*node_modules.*my-sleep.*index.js:\d+:\d+/)); + assert(stdout.match(/\bat\s+/g).length === 2); + assert(code === 1); + done(err); + }); + }); + }); });