From 558191d964e860eaddd6432e8fe5a86bad7781be Mon Sep 17 00:00:00 2001 From: Ariel Mashraki Date: Sun, 8 Mar 2015 00:18:44 +0200 Subject: [PATCH] feat(runner/stack-trace): solve issue #545 + test --- bin/_mocha | 5 ++ lib/mocha.js | 14 ++++++ lib/runner.js | 13 +++-- lib/utils.js | 65 +++++++++++++++++++++++++ mocha.js | 91 +++++++++++++++++++++++++++++++++-- test/browser/stack-trace.html | 24 +++++++++ test/browser/stack-trace.js | 20 ++++++++ test/runner.js | 50 ++++++++++++++++++- test/utils.js | 77 +++++++++++++++++++++++++++++ 9 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 test/browser/stack-trace.html create mode 100644 test/browser/stack-trace.js diff --git a/bin/_mocha b/bin/_mocha index 477b844f6f..45ec3cebce 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -73,6 +73,7 @@ program .option('-u, --ui ', 'specify user-interface (bdd|tdd|exports)', 'bdd') .option('-w, --watch', 'watch files for changes') .option('--check-leaks', 'check for global variable leaks') + .option('--full-trace', 'display the full stack trace') .option('--compilers :,...', 'use the given module(s) to compile files', list, []) .option('--debug-brk', "enable node's debugger breaking on the first line") .option('--globals ', 'allow the given comma-delimited global [names]', list, []) @@ -274,6 +275,10 @@ if (program.invert) mocha.invert(); if (program.checkLeaks) mocha.checkLeaks(); +// --stack-trace + +if(program.fullTrace) mocha.fullTrace(); + // --growl if (program.growl) mocha.growl(); diff --git a/lib/mocha.js b/lib/mocha.js index 61febc1ddb..3e29711a85 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -66,6 +66,7 @@ function image(name) { * - `bail` bail on the first test failure * - `slow` milliseconds to wait before considering a test slow * - `ignoreLeaks` ignore global leaks + * - `fullTrace` display the full stack-trace on failing * - `grep` string or regexp to filter tests with * * @param {Object} options @@ -265,6 +266,18 @@ Mocha.prototype.checkLeaks = function(){ return this; }; +/** + * Display long stack-trace on failing + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.fullTrace = function() { + this.options.fullStackTrace = true; + return this; +}; + /** * Enable growl support. * @@ -408,6 +421,7 @@ Mocha.prototype.run = function(fn){ var runner = new exports.Runner(suite, options.delay); var reporter = new this._reporter(runner, options); runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.fullStackTrace = options.fullStackTrace; runner.asyncOnly = options.asyncOnly; if (options.grep) runner.grep(options.grep, options.invert); if (options.globals) runner.globals(options.globals); diff --git a/lib/runner.js b/lib/runner.js index 7911896935..a555e8b69f 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -10,7 +10,8 @@ var EventEmitter = require('events').EventEmitter , filter = utils.filter , keys = utils.keys , type = utils.type - , stringify = utils.stringify; + , stringify = utils.stringify + , stackFilter = utils.stackTraceFilter(); /** * Non-enumerable globals. @@ -197,16 +198,18 @@ Runner.prototype.checkGlobals = function(test){ * @api private */ -Runner.prototype.fail = function(test, err){ +Runner.prototype.fail = function(test, err) { ++this.failures; test.state = 'failed'; - if ('string' == typeof err) { - err = new Error('the string "' + err + '" was thrown, throw an Error :)'); - } else if (!(err instanceof Error)) { + if (!(err instanceof Error)) { err = new Error('the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)'); } + err.stack = this.fullStackTrace + ? err.stack + : stackFilter(err.stack); + this.emit('fail', test, err); }; diff --git a/lib/utils.js b/lib/utils.js index bda5b8e84c..cb28b674e6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -629,3 +629,68 @@ exports.getError = function(err) { return err || exports.undefinedError(); }; + +/** + * @summary + * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) + * @description + * When invoking this function you get a filter function that get the Error.stack as an input, + * and return a prettify output. + * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace). + * @returns {Function} + */ + +exports.stackTraceFilter = function() { + var slash = '/' + , is = typeof document === 'undefined' + ? { node: true } + : { browser: true } + , cwd = is.node + ? process.cwd() + slash + : location.href.replace(/\/[^\/]*$/, '/'); + + function isNodeModule (line) { + return (~line.indexOf('node_modules')); + } + + function isMochaInternal (line) { + return (~line.indexOf('node_modules' + slash + 'mocha')) || + (~line.indexOf('components' + slash + 'mochajs')) || + (~line.indexOf('components' + slash + 'mocha')); + } + + // node_modules, bower, componentJS + function isBrowserModule(line) { + return (~line.indexOf('node_modules')) || + (~line.indexOf('components')); + } + + function isNodeInternal (line) { + return (~line.indexOf('(timers.js:')) || + (~line.indexOf('(events.js:')) || + (~line.indexOf('(node.js:')) || + (~line.indexOf('(module.js:')) || + (~line.indexOf('GeneratorFunctionPrototype.next (native)')) || + false + } + + return function(stack) { + stack = stack.split('\n'); + + stack = stack.reduce(function (list, line) { + if (is.node && (isNodeModule(line) || + isMochaInternal(line) || + isNodeInternal(line))) + return list; + + if (is.browser && (isBrowserModule(line))) + return list; + + // Clean up cwd(absolute) + list.push(line.replace(cwd, '')); + return list; + }, []); + + return stack.join('\n'); + } +}; \ No newline at end of file diff --git a/mocha.js b/mocha.js index e355cd9da6..bf761a7834 100644 --- a/mocha.js +++ b/mocha.js @@ -1470,6 +1470,7 @@ function image(name) { * - `bail` bail on the first test failure * - `slow` milliseconds to wait before considering a test slow * - `ignoreLeaks` ignore global leaks + * - `fullTrace` display the full stack-trace on failing * - `grep` string or regexp to filter tests with * * @param {Object} options @@ -1669,6 +1670,18 @@ Mocha.prototype.checkLeaks = function(){ return this; }; +/** + * Display long stack-trace on failing + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.fullTrace = function() { + this.options.fullStackTrace = true; + return this; +}; + /** * Enable growl support. * @@ -1812,6 +1825,7 @@ Mocha.prototype.run = function(fn){ var runner = new exports.Runner(suite, options.delay); var reporter = new this._reporter(runner, options); runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.fullStackTrace = options.fullStackTrace; runner.asyncOnly = options.asyncOnly; if (options.grep) runner.grep(options.grep, options.invert); if (options.globals) runner.globals(options.globals); @@ -4560,7 +4574,8 @@ var EventEmitter = require('browser/events').EventEmitter , filter = utils.filter , keys = utils.keys , type = utils.type - , stringify = utils.stringify; + , stringify = utils.stringify + , stackFilter = utils.stackTraceFilter(); /** * Non-enumerable globals. @@ -4751,16 +4766,18 @@ Runner.prototype.checkGlobals = function(test){ * @api private */ -Runner.prototype.fail = function(test, err){ +Runner.prototype.fail = function(test, err) { ++this.failures; test.state = 'failed'; - if ('string' == typeof err) { - err = new Error('the string "' + err + '" was thrown, throw an Error :)'); - } else if (!(err instanceof Error)) { + if (!(err instanceof Error)) { err = new Error('the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)'); } + err.stack = this.fullStackTrace + ? err.stack + : stackFilter(err.stack); + this.emit('fail', test, err); }; @@ -6304,6 +6321,70 @@ exports.getError = function(err) { }; +/** + * @summary + * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) + * @description + * When invoking this function you get a filter function that get the Error.stack as an input, + * and return a prettify output. + * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace). + * @returns {Function} + */ + +exports.stackTraceFilter = function() { + var slash = '/' + , is = typeof document === 'undefined' + ? { node: true } + : { browser: true } + , cwd = is.node + ? process.cwd() + slash + : location.href.replace(/\/[^\/]*$/, '/'); + + function isNodeModule (line) { + return (~line.indexOf('node_modules')); + } + + function isMochaInternal (line) { + return (~line.indexOf('node_modules' + slash + 'mocha')) || + (~line.indexOf('components' + slash + 'mochajs')) || + (~line.indexOf('components' + slash + 'mocha')); + } + + // node_modules, bower, componentJS + function isBrowserModule(line) { + return (~line.indexOf('node_modules')) || + (~line.indexOf('components')); + } + + function isNodeInternal (line) { + return (~line.indexOf('(timers.js:')) || + (~line.indexOf('(events.js:')) || + (~line.indexOf('(node.js:')) || + (~line.indexOf('(module.js:')) || + (~line.indexOf('GeneratorFunctionPrototype.next (native)')) || + false + } + + return function(stack) { + stack = stack.split('\n'); + + stack = stack.reduce(function (list, line) { + if (is.node && (isNodeModule(line) || + isMochaInternal(line) || + isNodeInternal(line))) + return list; + + if (is.browser && (isBrowserModule(line))) + return list; + + // Clean up cwd(absolute) + list.push(line.replace(cwd, '')); + return list; + }, []); + + return stack.join('\n'); + } +}; }); // module: utils.js // The global object is "self" in Web Workers. var global = (function() { return this; })(); diff --git a/test/browser/stack-trace.html b/test/browser/stack-trace.html new file mode 100644 index 0000000000..0f267dab98 --- /dev/null +++ b/test/browser/stack-trace.html @@ -0,0 +1,24 @@ + + + Mocha + + + + + + + + + + +
+ + diff --git a/test/browser/stack-trace.js b/test/browser/stack-trace.js new file mode 100644 index 0000000000..b39944db09 --- /dev/null +++ b/test/browser/stack-trace.js @@ -0,0 +1,20 @@ +'use strict'; +describe('Stack trace', function() { + it('should prettify the stack-trace', function() { + var err = new Error(); + // We do this fake stack-trace because we under development, + // and our root isn't `node_modules`, `bower` or `components` + err.stack = ['Error: failed' + , 'at assert (stack-trace.html:11:30)' + , 'at Context. (stack-trace.js:5:5)' + , 'at callFn (http://localhost:63342/node_modules/mocha.js:4546:21)' + , 'at Test.require.register.Runnable.run (http://localhost:63342/node_modules/mocha.js:4539:7)' + , 'at Runner.require.register.Runner.runTest (http://localhost:63342/node_modules/mocha.js:4958:10)' + , 'at http://localhost:63342/bower_components/mocha.js:5041:12' + , 'at next (http://localhost:63342/bower_components/mocha.js:4883:14)' + , 'at http://localhost:63342/bower_components/mocha.js:4893:7' + , 'at next (http://localhost:63342/bower_components/mocha.js:4828:23)' + , 'at http://localhost:63342/bower_components/mocha.js:4860:5'].join('\n'); + assert(false, err); + }); +}); \ No newline at end of file diff --git a/test/runner.js b/test/runner.js index 89cb8216b4..a2ce512514 100644 --- a/test/runner.js +++ b/test/runner.js @@ -290,5 +290,51 @@ describe('Runner', function(){ runner.failHook(hook, err); done(); }) - }) -}) + }); + + describe('stackTrace', function() { + var stack = [ 'AssertionError: foo bar' + , 'at EventEmitter. (/usr/local/dev/test.js:16:12)' + , 'at Context. (/usr/local/dev/test.js:19:5)' + , 'Test.Runnable.run (/usr/local/lib/node_modules/mocha/lib/runnable.js:244:7)' + , 'Runner.runTest (/usr/local/lib/node_modules/mocha/lib/runner.js:374:10)' + , '/usr/local/lib/node_modules/mocha/lib/runner.js:452:12' + , 'next (/usr/local/lib/node_modules/mocha/lib/runner.js:299:14)' + , '/usr/local/lib/node_modules/mocha/lib/runner.js:309:7' + , 'next (/usr/local/lib/node_modules/mocha/lib/runner.js:248:23)' + , 'Immediate._onImmediate (/usr/local/lib/node_modules/mocha/lib/runner.js:276:5)' + , 'at processImmediate [as _immediateCallback] (timers.js:321:17)']; + + describe('shortStackTrace', function() { + it('should prettify the stack-trace', function(done) { + var hook = {}, + err = new Error(); + // Fake stack-trace + err.stack = stack.join('\n'); + + runner.on('fail', function(hook, err){ + err.stack.should.equal(stack.slice(0,3).join('\n')); + done(); + }); + runner.failHook(hook, err); + }); + }); + + describe('longStackTrace', function() { + it('should display the full stack-trace', function(done) { + var hook = {}, + err = new Error(); + // Fake stack-trace + err.stack = stack.join('\n'); + // Add --stack-trace option + runner.fullStackTrace = true; + + runner.on('fail', function(hook, err){ + err.stack.should.equal(stack.join('\n')); + done(); + }); + runner.failHook(hook, err); + }); + }); + }); +}); diff --git a/test/utils.js b/test/utils.js index f60b42fc9f..780d8c324c 100644 --- a/test/utils.js +++ b/test/utils.js @@ -64,4 +64,81 @@ describe('utils', function() { }); }) }); + + describe('.stackTraceFilter()', function() { + describe('on node', function() { + var filter = utils.stackTraceFilter(); + it('should get a stack-trace as a string and prettify it', function() { + var stack = [ 'AssertionError: foo bar' + , 'at EventEmitter. (/usr/local/dev/test.js:16:12)' + , 'at Context. (/usr/local/dev/test.js:19:5)' + , 'Test.Runnable.run (/usr/local/lib/node_modules/mocha/lib/runnable.js:244:7)' + , 'Runner.runTest (/usr/local/lib/node_modules/mocha/lib/runner.js:374:10)' + , '/usr/local/lib/node_modules/mocha/lib/runner.js:452:12' + , 'next (/usr/local/lib/node_modules/mocha/lib/runner.js:299:14)' + , '/usr/local/lib/node_modules/mocha/lib/runner.js:309:7' + , 'next (/usr/local/lib/node_modules/mocha/lib/runner.js:248:23)' + , 'Immediate._onImmediate (/usr/local/lib/node_modules/mocha/lib/runner.js:276:5)' + , 'at processImmediate [as _immediateCallback] (timers.js:321:17)']; + filter(stack.join('\n')).should.equal(stack.slice(0,3).join('\n')); + + stack = [ 'AssertionError: bar baz' + , 'at /usr/local/dev/some-test-file.js:25:8' + , 'at tryCatcher (/usr/local/dev/own/tmp/node_modules/bluebird/js/main/util.js:24:31)' + , 'at Promise._resolveFromResolver (/usr/local/dev/own/tmp/node_modules/bluebird/js/main/promise.js:439:31)' + , 'at new Promise (/usr/local/dev/own/tmp/node_modules/bluebird/js/main/promise.js:53:37)' + , 'at yourFunction (/usr/local/dev/own/tmp/test1.js:24:13)' + , 'at Context. (/usr/local/dev/some-test-file:30:4)' + , 'Test.Runnable.run (/usr/local/lib/node_modules/mocha/lib/runnable.js:218:15)' + , 'next (/usr/local/lib/node_modules/mocha/lib/runner.js:248:23)' + , 'Immediate._onImmediate (/usr/local/lib/node_modules/mocha/lib/runner.js:276:5)' + , 'at processImmediate [as _immediateCallback] (timers.js:321:17)']; + filter(stack.join('\n')).should.equal(stack.slice(0,2).concat(stack.slice(5,7)).join('\n')); + }); + + it('should ignore bower and components files', function() { + var stack = ['Error: failed' + , 'at assert (index.html:11:26)' + , 'at Context. (test.js:17:18)' + , 'at bower_components/should/should.js:4827:7' + , 'at next (file:///.../bower_components/should/should.js:4766:23)' + , 'at components/should/5.0.0/should.js:4827:7' + , 'at next (file:///.../components/should/5.0.0/should.js:4766:23)' + , 'at file:///.../bower_components/mocha/mocha.js:4794:5' + , 'at timeslice (.../components/mocha/mocha.js:6218:27)' + , 'at Test.require.register.Runnable.run (file:///.../components/mochajs/mocha/2.1.0/mocha.js:4463:15)' + , 'at Runner.require.register.Runner.runTest (file:///.../components/mochajs/mocha/2.1.0/mocha.js:4892:10)' + , 'at file:///.../components/mochajs/mocha/2.1.0/mocha.js:4970:12' + , 'at next (file:///.../components/mochajs/mocha/2.1.0/mocha.js:4817:14)']; + filter(stack.join('\n')).should.equal(stack.slice(0,7).join('\n')); + }); + }); + + describe('on browser', function() { + var filter; + before(function() { + global.document = true; + global.location = { href: 'localhost:3000/foo/bar/index.html' }; + filter = utils.stackTraceFilter(); + }); + it('should strip out bower and components too', function() { + var stack = ['Error: failed' + , 'at assert (index.html:11:26)' + , 'at Context. (test.js:17:18)' + , 'at bower_components/should/should.js:4827:7' + , 'at next (localhost:3000/foo/bar/bower_components/should/should.js:4766:23)' + , 'at components/should/5.0.0/should.js:4827:7' + , 'at next (localhost:3000/foo/bar/components/should/5.0.0/should.js:4766:23)' + , 'at Runner.require.register.Runner.runTest (localhost:3000/foo/bar/node_modules/mocha.js:4892:10)' + , 'at localhost:3000/foo/bar/node_modules/mocha.js:4970:12' + , 'at next (localhost:3000/foo/bar/node_modules/mocha.js:4817:14)']; + filter(stack.join('\n')).should.equal(stack.slice(0,3).join('\n')); + }); + + after(function() { + delete global.document; + delete global.location; + }); + }); + }); });