source: Dev/trunk/node_modules/mocha/lib/runner.js @ 484

Last change on this file since 484 was 484, checked in by hendrikvanantwerpen, 11 years ago

Commit node_modules, to make checkouts and builds more deterministic.

File size: 11.4 KB
Line 
1/**
2 * Module dependencies.
3 */
4
5var EventEmitter = require('events').EventEmitter
6  , debug = require('debug')('mocha:runner')
7  , Test = require('./test')
8  , utils = require('./utils')
9  , filter = utils.filter
10  , keys = utils.keys;
11
12/**
13 * Non-enumerable globals.
14 */
15
16var globals = [
17  'setTimeout',
18  'clearTimeout',
19  'setInterval',
20  'clearInterval',
21  'XMLHttpRequest',
22  'Date'
23];
24
25/**
26 * Expose `Runner`.
27 */
28
29module.exports = Runner;
30
31/**
32 * Initialize a `Runner` for the given `suite`.
33 *
34 * Events:
35 *
36 *   - `start`  execution started
37 *   - `end`  execution complete
38 *   - `suite`  (suite) test suite execution started
39 *   - `suite end`  (suite) all tests (and sub-suites) have finished
40 *   - `test`  (test) test execution started
41 *   - `test end`  (test) test completed
42 *   - `hook`  (hook) hook execution started
43 *   - `hook end`  (hook) hook complete
44 *   - `pass`  (test) test passed
45 *   - `fail`  (test, err) test failed
46 *   - `pending`  (test) test pending
47 *
48 * @api public
49 */
50
51function Runner(suite) {
52  var self = this;
53  this._globals = [];
54  this.suite = suite;
55  this.total = suite.total();
56  this.failures = 0;
57  this.on('test end', function(test){ self.checkGlobals(test); });
58  this.on('hook end', function(hook){ self.checkGlobals(hook); });
59  this.grep(/.*/);
60  this.globals(this.globalProps().concat(['errno']));
61}
62
63/**
64 * Wrapper for setImmediate, process.nextTick, or browser polyfill.
65 *
66 * @param {Function} fn
67 * @api private
68 */
69
70Runner.immediately = global.setImmediate || process.nextTick;
71
72/**
73 * Inherit from `EventEmitter.prototype`.
74 */
75
76Runner.prototype.__proto__ = EventEmitter.prototype;
77
78/**
79 * Run tests with full titles matching `re`. Updates runner.total
80 * with number of tests matched.
81 *
82 * @param {RegExp} re
83 * @param {Boolean} invert
84 * @return {Runner} for chaining
85 * @api public
86 */
87
88Runner.prototype.grep = function(re, invert){
89  debug('grep %s', re);
90  this._grep = re;
91  this._invert = invert;
92  this.total = this.grepTotal(this.suite);
93  return this;
94};
95
96/**
97 * Returns the number of tests matching the grep search for the
98 * given suite.
99 *
100 * @param {Suite} suite
101 * @return {Number}
102 * @api public
103 */
104
105Runner.prototype.grepTotal = function(suite) {
106  var self = this;
107  var total = 0;
108
109  suite.eachTest(function(test){
110    var match = self._grep.test(test.fullTitle());
111    if (self._invert) match = !match;
112    if (match) total++;
113  });
114
115  return total;
116};
117
118/**
119 * Return a list of global properties.
120 *
121 * @return {Array}
122 * @api private
123 */
124
125Runner.prototype.globalProps = function() {
126  var props = utils.keys(global);
127
128  // non-enumerables
129  for (var i = 0; i < globals.length; ++i) {
130    if (~utils.indexOf(props, globals[i])) continue;
131    props.push(globals[i]);
132  }
133
134  return props;
135};
136
137/**
138 * Allow the given `arr` of globals.
139 *
140 * @param {Array} arr
141 * @return {Runner} for chaining
142 * @api public
143 */
144
145Runner.prototype.globals = function(arr){
146  if (0 == arguments.length) return this._globals;
147  debug('globals %j', arr);
148  utils.forEach(arr, function(arr){
149    this._globals.push(arr);
150  }, this);
151  return this;
152};
153
154/**
155 * Check for global variable leaks.
156 *
157 * @api private
158 */
159
160Runner.prototype.checkGlobals = function(test){
161  if (this.ignoreLeaks) return;
162  var ok = this._globals;
163  var globals = this.globalProps();
164  var isNode = process.kill;
165  var leaks;
166
167  // check length - 2 ('errno' and 'location' globals)
168  if (isNode && 1 == ok.length - globals.length) return;
169  else if (2 == ok.length - globals.length) return;
170
171  if(this.prevGlobalsLength == globals.length) return;
172  this.prevGlobalsLength = globals.length;
173
174  leaks = filterLeaks(ok, globals);
175  this._globals = this._globals.concat(leaks);
176
177  if (leaks.length > 1) {
178    this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + ''));
179  } else if (leaks.length) {
180    this.fail(test, new Error('global leak detected: ' + leaks[0]));
181  }
182};
183
184/**
185 * Fail the given `test`.
186 *
187 * @param {Test} test
188 * @param {Error} err
189 * @api private
190 */
191
192Runner.prototype.fail = function(test, err){
193  ++this.failures;
194  test.state = 'failed';
195
196  if ('string' == typeof err) {
197    err = new Error('the string "' + err + '" was thrown, throw an Error :)');
198  }
199
200  this.emit('fail', test, err);
201};
202
203/**
204 * Fail the given `hook` with `err`.
205 *
206 * Hook failures (currently) hard-end due
207 * to that fact that a failing hook will
208 * surely cause subsequent tests to fail,
209 * causing jumbled reporting.
210 *
211 * @param {Hook} hook
212 * @param {Error} err
213 * @api private
214 */
215
216Runner.prototype.failHook = function(hook, err){
217  this.fail(hook, err);
218  this.emit('end');
219};
220
221/**
222 * Run hook `name` callbacks and then invoke `fn()`.
223 *
224 * @param {String} name
225 * @param {Function} function
226 * @api private
227 */
228
229Runner.prototype.hook = function(name, fn){
230  var suite = this.suite
231    , hooks = suite['_' + name]
232    , self = this
233    , timer;
234
235  function next(i) {
236    var hook = hooks[i];
237    if (!hook) return fn();
238    if (self.failures && suite.bail()) return fn();
239    self.currentRunnable = hook;
240
241    hook.ctx.currentTest = self.test;
242
243    self.emit('hook', hook);
244
245    hook.on('error', function(err){
246      self.failHook(hook, err);
247    });
248
249    hook.run(function(err){
250      hook.removeAllListeners('error');
251      var testError = hook.error();
252      if (testError) self.fail(self.test, testError);
253      if (err) return self.failHook(hook, err);
254      self.emit('hook end', hook);
255      delete hook.ctx.currentTest;
256      next(++i);
257    });
258  }
259
260  Runner.immediately(function(){
261    next(0);
262  });
263};
264
265/**
266 * Run hook `name` for the given array of `suites`
267 * in order, and callback `fn(err)`.
268 *
269 * @param {String} name
270 * @param {Array} suites
271 * @param {Function} fn
272 * @api private
273 */
274
275Runner.prototype.hooks = function(name, suites, fn){
276  var self = this
277    , orig = this.suite;
278
279  function next(suite) {
280    self.suite = suite;
281
282    if (!suite) {
283      self.suite = orig;
284      return fn();
285    }
286
287    self.hook(name, function(err){
288      if (err) {
289        self.suite = orig;
290        return fn(err);
291      }
292
293      next(suites.pop());
294    });
295  }
296
297  next(suites.pop());
298};
299
300/**
301 * Run hooks from the top level down.
302 *
303 * @param {String} name
304 * @param {Function} fn
305 * @api private
306 */
307
308Runner.prototype.hookUp = function(name, fn){
309  var suites = [this.suite].concat(this.parents()).reverse();
310  this.hooks(name, suites, fn);
311};
312
313/**
314 * Run hooks from the bottom up.
315 *
316 * @param {String} name
317 * @param {Function} fn
318 * @api private
319 */
320
321Runner.prototype.hookDown = function(name, fn){
322  var suites = [this.suite].concat(this.parents());
323  this.hooks(name, suites, fn);
324};
325
326/**
327 * Return an array of parent Suites from
328 * closest to furthest.
329 *
330 * @return {Array}
331 * @api private
332 */
333
334Runner.prototype.parents = function(){
335  var suite = this.suite
336    , suites = [];
337  while (suite = suite.parent) suites.push(suite);
338  return suites;
339};
340
341/**
342 * Run the current test and callback `fn(err)`.
343 *
344 * @param {Function} fn
345 * @api private
346 */
347
348Runner.prototype.runTest = function(fn){
349  var test = this.test
350    , self = this;
351
352  if (this.asyncOnly) test.asyncOnly = true;
353
354  try {
355    test.on('error', function(err){
356      self.fail(test, err);
357    });
358    test.run(fn);
359  } catch (err) {
360    fn(err);
361  }
362};
363
364/**
365 * Run tests in the given `suite` and invoke
366 * the callback `fn()` when complete.
367 *
368 * @param {Suite} suite
369 * @param {Function} fn
370 * @api private
371 */
372
373Runner.prototype.runTests = function(suite, fn){
374  var self = this
375    , tests = suite.tests.slice()
376    , test;
377
378  function next(err) {
379    // if we bail after first err
380    if (self.failures && suite._bail) return fn();
381
382    // next test
383    test = tests.shift();
384
385    // all done
386    if (!test) return fn();
387
388    // grep
389    var match = self._grep.test(test.fullTitle());
390    if (self._invert) match = !match;
391    if (!match) return next();
392
393    // pending
394    if (test.pending) {
395      self.emit('pending', test);
396      self.emit('test end', test);
397      return next();
398    }
399
400    // execute test and hook(s)
401    self.emit('test', self.test = test);
402    self.hookDown('beforeEach', function(){
403      self.currentRunnable = self.test;
404      self.runTest(function(err){
405        test = self.test;
406
407        if (err) {
408          self.fail(test, err);
409          self.emit('test end', test);
410          return self.hookUp('afterEach', next);
411        }
412
413        test.state = 'passed';
414        self.emit('pass', test);
415        self.emit('test end', test);
416        self.hookUp('afterEach', next);
417      });
418    });
419  }
420
421  this.next = next;
422  next();
423};
424
425/**
426 * Run the given `suite` and invoke the
427 * callback `fn()` when complete.
428 *
429 * @param {Suite} suite
430 * @param {Function} fn
431 * @api private
432 */
433
434Runner.prototype.runSuite = function(suite, fn){
435  var total = this.grepTotal(suite)
436    , self = this
437    , i = 0;
438
439  debug('run suite %s', suite.fullTitle());
440
441  if (!total) return fn();
442
443  this.emit('suite', this.suite = suite);
444
445  function next() {
446    var curr = suite.suites[i++];
447    if (!curr) return done();
448    self.runSuite(curr, next);
449  }
450
451  function done() {
452    self.suite = suite;
453    self.hook('afterAll', function(){
454      self.emit('suite end', suite);
455      fn();
456    });
457  }
458
459  this.hook('beforeAll', function(){
460    self.runTests(suite, next);
461  });
462};
463
464/**
465 * Handle uncaught exceptions.
466 *
467 * @param {Error} err
468 * @api private
469 */
470
471Runner.prototype.uncaught = function(err){
472  debug('uncaught exception %s', err.message);
473  var runnable = this.currentRunnable;
474  if (!runnable || 'failed' == runnable.state) return;
475  runnable.clearTimeout();
476  err.uncaught = true;
477  this.fail(runnable, err);
478
479  // recover from test
480  if ('test' == runnable.type) {
481    this.emit('test end', runnable);
482    this.hookUp('afterEach', this.next);
483    return;
484  }
485
486  // bail on hooks
487  this.emit('end');
488};
489
490/**
491 * Run the root suite and invoke `fn(failures)`
492 * on completion.
493 *
494 * @param {Function} fn
495 * @return {Runner} for chaining
496 * @api public
497 */
498
499Runner.prototype.run = function(fn){
500  var self = this
501    , fn = fn || function(){};
502
503  function uncaught(err){
504    self.uncaught(err);
505  }
506
507  debug('start');
508
509  // callback
510  this.on('end', function(){
511    debug('end');
512    process.removeListener('uncaughtException', uncaught);
513    fn(self.failures);
514  });
515
516  // run suites
517  this.emit('start');
518  this.runSuite(this.suite, function(){
519    debug('finished running');
520    self.emit('end');
521  });
522
523  // uncaught exception
524  process.on('uncaughtException', uncaught);
525
526  return this;
527};
528
529/**
530 * Filter leaks with the given globals flagged as `ok`.
531 *
532 * @param {Array} ok
533 * @param {Array} globals
534 * @return {Array}
535 * @api private
536 */
537
538function filterLeaks(ok, globals) {
539  return filter(globals, function(key){
540    // Firefox and Chrome exposes iframes as index inside the window object
541    if (/^d+/.test(key)) return false;
542
543    // in firefox
544    // if runner runs in an iframe, this iframe's window.getInterface method not init at first
545    // it is assigned in some seconds
546    if (global.navigator && /^getInterface/.test(key)) return false;
547
548    // an iframe could be approached by window[iframeIndex]
549    // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak
550    if (global.navigator && /^\d+/.test(key)) return false;
551
552    // Opera and IE expose global variables for HTML element IDs (issue #243)
553    if (/^mocha-/.test(key)) return false;
554
555    var matched = filter(ok, function(ok){
556      if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
557      return key == ok;
558    });
559    return matched.length == 0 && (!global.navigator || 'onerror' !== key);
560  });
561}
Note: See TracBrowser for help on using the repository browser.