'use strict';

/**

* Module dependencies.
*/

var util = require('util'); var EventEmitter = require('events').EventEmitter; var Pending = require('./pending'); var utils = require('./utils'); var inherits = utils.inherits; var debug = require('debug')('mocha:runner'); var Runnable = require('./runnable'); var Suite = require('./suite'); var HOOK_TYPE_BEFORE_EACH = Suite.constants.HOOK_TYPE_BEFORE_EACH; var HOOK_TYPE_AFTER_EACH = Suite.constants.HOOK_TYPE_AFTER_EACH; var HOOK_TYPE_AFTER_ALL = Suite.constants.HOOK_TYPE_AFTER_ALL; var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL; var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN; var STATE_FAILED = Runnable.constants.STATE_FAILED; var STATE_PASSED = Runnable.constants.STATE_PASSED; var dQuote = utils.dQuote; var ngettext = utils.ngettext; var sQuote = utils.sQuote; var stackFilter = utils.stackTraceFilter(); var stringify = utils.stringify; var type = utils.type; var createInvalidExceptionError = require('./errors')

.createInvalidExceptionError;

/**

* Non-enumerable globals.
* @readonly
*/

var globals = [

'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'XMLHttpRequest',
'Date',
'setImmediate',
'clearImmediate'

];

var constants = utils.defineConstants(

/**
 * {@link Runner}-related constants.
 * @public
 * @memberof Runner
 * @readonly
 * @alias constants
 * @static
 * @enum {string}
 */
{
  /**
   * Emitted when {@link Hook} execution begins
   */
  EVENT_HOOK_BEGIN: 'hook',
  /**
   * Emitted when {@link Hook} execution ends
   */
  EVENT_HOOK_END: 'hook end',
  /**
   * Emitted when Root {@link Suite} execution begins (all files have been parsed and hooks/tests are ready for execution)
   */
  EVENT_RUN_BEGIN: 'start',
  /**
   * Emitted when Root {@link Suite} execution has been delayed via `delay` option
   */
  EVENT_DELAY_BEGIN: 'waiting',
  /**
   * Emitted when delayed Root {@link Suite} execution is triggered by user via `global.run()`
   */
  EVENT_DELAY_END: 'ready',
  /**
   * Emitted when Root {@link Suite} execution ends
   */
  EVENT_RUN_END: 'end',
  /**
   * Emitted when {@link Suite} execution begins
   */
  EVENT_SUITE_BEGIN: 'suite',
  /**
   * Emitted when {@link Suite} execution ends
   */
  EVENT_SUITE_END: 'suite end',
  /**
   * Emitted when {@link Test} execution begins
   */
  EVENT_TEST_BEGIN: 'test',
  /**
   * Emitted when {@link Test} execution ends
   */
  EVENT_TEST_END: 'test end',
  /**
   * Emitted when {@link Test} execution fails
   */
  EVENT_TEST_FAIL: 'fail',
  /**
   * Emitted when {@link Test} execution succeeds
   */
  EVENT_TEST_PASS: 'pass',
  /**
   * Emitted when {@link Test} becomes pending
   */
  EVENT_TEST_PENDING: 'pending',
  /**
   * Emitted when {@link Test} execution has failed, but will retry
   */
  EVENT_TEST_RETRY: 'retry'
}

);

module.exports = Runner;

/**

* Initialize a `Runner` at the Root {@link Suite}, which represents a hierarchy of {@link Suite|Suites} and {@link Test|Tests}.
*
* @extends external:EventEmitter
* @public
* @class
* @param {Suite} suite Root suite
* @param {boolean} [delay] Whether or not to delay execution of root suite
* until ready.
*/

function Runner(suite, delay) {

var self = this;
this._globals = [];
this._abort = false;
this._delay = delay;
this.suite = suite;
this.started = false;
this.total = suite.total();
this.failures = 0;
this.on(constants.EVENT_TEST_END, function(test) {
  self.checkGlobals(test);
});
this.on(constants.EVENT_HOOK_END, function(hook) {
  self.checkGlobals(hook);
});
this._defaultGrep = /.*/;
this.grep(this._defaultGrep);
this.globals(this.globalProps());

}

/**

* Wrapper for setImmediate, process.nextTick, or browser polyfill.
*
* @param {Function} fn
* @private
*/

Runner.immediately = global.setImmediate || process.nextTick;

/**

* Inherit from `EventEmitter.prototype`.
*/

inherits(Runner, EventEmitter);

/**

* Run tests with full titles matching `re`. Updates runner.total
* with number of tests matched.
*
* @public
* @memberof Runner
* @param {RegExp} re
* @param {boolean} invert
* @return {Runner} Runner instance.
*/

Runner.prototype.grep = function(re, invert) {

debug('grep %s', re);
this._grep = re;
this._invert = invert;
this.total = this.grepTotal(this.suite);
return this;

};

/**

* Returns the number of tests matching the grep search for the
* given suite.
*
* @memberof Runner
* @public
* @param {Suite} suite
* @return {number}
*/

Runner.prototype.grepTotal = function(suite) {

var self = this;
var total = 0;

suite.eachTest(function(test) {
  var match = self._grep.test(test.fullTitle());
  if (self._invert) {
    match = !match;
  }
  if (match) {
    total++;
  }
});

return total;

};

/**

* Return a list of global properties.
*
* @return {Array}
* @private
*/

Runner.prototype.globalProps = function() {

var props = Object.keys(global);

// non-enumerables
for (var i = 0; i < globals.length; ++i) {
  if (~props.indexOf(globals[i])) {
    continue;
  }
  props.push(globals[i]);
}

return props;

};

/**

* Allow the given `arr` of globals.
*
* @public
* @memberof Runner
* @param {Array} arr
* @return {Runner} Runner instance.
*/

Runner.prototype.globals = function(arr) {

if (!arguments.length) {
  return this._globals;
}
debug('globals %j', arr);
this._globals = this._globals.concat(arr);
return this;

};

/**

* Check for global variable leaks.
*
* @private
*/

Runner.prototype.checkGlobals = function(test) {

if (this.ignoreLeaks) {
  return;
}
var ok = this._globals;

var globals = this.globalProps();
var leaks;

if (test) {
  ok = ok.concat(test._allowedGlobals || []);
}

if (this.prevGlobalsLength === globals.length) {
  return;
}
this.prevGlobalsLength = globals.length;

leaks = filterLeaks(ok, globals);
this._globals = this._globals.concat(leaks);

if (leaks.length) {
  var format = ngettext(
    leaks.length,
    'global leak detected: %s',
    'global leaks detected: %s'
  );
  var error = new Error(util.format(format, leaks.map(sQuote).join(', ')));
  this.fail(test, error);
}

};

/**

* Fail the given `test`.
*
* @private
* @param {Test} test
* @param {Error} err
*/

Runner.prototype.fail = function(test, err) {

if (test.isPending()) {
  return;
}

++this.failures;
test.state = STATE_FAILED;

if (!isError(err)) {
  err = thrown2Error(err);
}

try {
  err.stack =
    this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack);
} catch (ignore) {
  // some environments do not take kindly to monkeying with the stack
}

this.emit(constants.EVENT_TEST_FAIL, test, err);

};

/**

* Fail the given `hook` with `err`.
*
* Hook failures work in the following pattern:
* - If bail, run corresponding `after each` and `after` hooks,
*   then exit
* - Failed `before` hook skips all tests in a suite and subsuites,
*   but jumps to corresponding `after` hook
* - Failed `before each` hook skips remaining tests in a
*   suite and jumps to corresponding `after each` hook,
*   which is run only once
* - Failed `after` hook does not alter
*   execution order
* - Failed `after each` hook skips remaining tests in a
*   suite and subsuites, but executes other `after each`
*   hooks
*
* @private
* @param {Hook} hook
* @param {Error} err
*/

Runner.prototype.failHook = function(hook, err) {

hook.originalTitle = hook.originalTitle || hook.title;
if (hook.ctx && hook.ctx.currentTest) {
  hook.title =
    hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title);
} else {
  var parentTitle;
  if (hook.parent.title) {
    parentTitle = hook.parent.title;
  } else {
    parentTitle = hook.parent.root ? '{root}' : '';
  }
  hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle);
}

this.fail(hook, err);

};

/**

* Run hook `name` callbacks and then invoke `fn()`.
*
* @private
* @param {string} name
* @param {Function} fn
*/

Runner.prototype.hook = function(name, fn) {

var suite = this.suite;
var hooks = suite.getHooks(name);
var self = this;

function next(i) {
  var hook = hooks[i];
  if (!hook) {
    return fn();
  }
  self.currentRunnable = hook;

  if (name === HOOK_TYPE_BEFORE_ALL) {
    hook.ctx.currentTest = hook.parent.tests[0];
  } else if (name === HOOK_TYPE_AFTER_ALL) {
    hook.ctx.currentTest = hook.parent.tests[hook.parent.tests.length - 1];
  } else {
    hook.ctx.currentTest = self.test;
  }

  hook.allowUncaught = self.allowUncaught;

  self.emit(constants.EVENT_HOOK_BEGIN, hook);

  if (!hook.listeners('error').length) {
    hook.on('error', function(err) {
      self.failHook(hook, err);
    });
  }

  hook.run(function(err) {
    var testError = hook.error();
    if (testError) {
      self.fail(self.test, testError);
    }
    if (err) {
      if (err instanceof Pending) {
        if (name === HOOK_TYPE_AFTER_ALL) {
          utils.deprecate(
            'Skipping a test within an "after all" hook is DEPRECATED and will throw an exception in a future version of Mocha. ' +
              'Use a return statement or other means to abort hook execution.'
          );
        }
        if (name === HOOK_TYPE_BEFORE_EACH || name === HOOK_TYPE_AFTER_EACH) {
          if (self.test) {
            self.test.pending = true;
          }
        } else {
          suite.tests.forEach(function(test) {
            test.pending = true;
          });
          suite.suites.forEach(function(suite) {
            suite.pending = true;
          });
          // a pending hook won't be executed twice.
          hook.pending = true;
        }
      } else {
        self.failHook(hook, err);

        // stop executing hooks, notify callee of hook err
        return fn(err);
      }
    }
    self.emit(constants.EVENT_HOOK_END, hook);
    delete hook.ctx.currentTest;
    next(++i);
  });
}

Runner.immediately(function() {
  next(0);
});

};

/**

* Run hook `name` for the given array of `suites`
* in order, and callback `fn(err, errSuite)`.
*
* @private
* @param {string} name
* @param {Array} suites
* @param {Function} fn
*/

Runner.prototype.hooks = function(name, suites, fn) {

var self = this;
var orig = this.suite;

function next(suite) {
  self.suite = suite;

  if (!suite) {
    self.suite = orig;
    return fn();
  }

  self.hook(name, function(err) {
    if (err) {
      var errSuite = self.suite;
      self.suite = orig;
      return fn(err, errSuite);
    }

    next(suites.pop());
  });
}

next(suites.pop());

};

/**

* Run hooks from the top level down.
*
* @param {String} name
* @param {Function} fn
* @private
*/

Runner.prototype.hookUp = function(name, fn) {

var suites = [this.suite].concat(this.parents()).reverse();
this.hooks(name, suites, fn);

};

/**

* Run hooks from the bottom up.
*
* @param {String} name
* @param {Function} fn
* @private
*/

Runner.prototype.hookDown = function(name, fn) {

var suites = [this.suite].concat(this.parents());
this.hooks(name, suites, fn);

};

/**

* Return an array of parent Suites from
* closest to furthest.
*
* @return {Array}
* @private
*/

Runner.prototype.parents = function() {

var suite = this.suite;
var suites = [];
while (suite.parent) {
  suite = suite.parent;
  suites.push(suite);
}
return suites;

};

/**

* Run the current test and callback `fn(err)`.
*
* @param {Function} fn
* @private
*/

Runner.prototype.runTest = function(fn) {

var self = this;
var test = this.test;

if (!test) {
  return;
}

var suite = this.parents().reverse()[0] || this.suite;
if (this.forbidOnly && suite.hasOnly()) {
  fn(new Error('`.only` forbidden'));
  return;
}
if (this.asyncOnly) {
  test.asyncOnly = true;
}
test.on('error', function(err) {
  self.fail(test, err);
});
if (this.allowUncaught) {
  test.allowUncaught = true;
  return test.run(fn);
}
try {
  test.run(fn);
} catch (err) {
  fn(err);
}

};

/**

* Run tests in the given `suite` and invoke the callback `fn()` when complete.
*
* @private
* @param {Suite} suite
* @param {Function} fn
*/

Runner.prototype.runTests = function(suite, fn) {

var self = this;
var tests = suite.tests.slice();
var test;

function hookErr(_, errSuite, after) {
  // before/after Each hook for errSuite failed:
  var orig = self.suite;

  // for failed 'after each' hook start from errSuite parent,
  // otherwise start from errSuite itself
  self.suite = after ? errSuite.parent : errSuite;

  if (self.suite) {
    // call hookUp afterEach
    self.hookUp(HOOK_TYPE_AFTER_EACH, function(err2, errSuite2) {
      self.suite = orig;
      // some hooks may fail even now
      if (err2) {
        return hookErr(err2, errSuite2, true);
      }
      // report error suite
      fn(errSuite);
    });
  } else {
    // there is no need calling other 'after each' hooks
    self.suite = orig;
    fn(errSuite);
  }
}

function next(err, errSuite) {
  // if we bail after first err
  if (self.failures && suite._bail) {
    tests = [];
  }

  if (self._abort) {
    return fn();
  }

  if (err) {
    return hookErr(err, errSuite, true);
  }

  // next test
  test = tests.shift();

  // all done
  if (!test) {
    return fn();
  }

  // grep
  var match = self._grep.test(test.fullTitle());
  if (self._invert) {
    match = !match;
  }
  if (!match) {
    // Run immediately only if we have defined a grep. When we
    // define a grep — It can cause maximum callstack error if
    // the grep is doing a large recursive loop by neglecting
    // all tests. The run immediately function also comes with
    // a performance cost. So we don't want to run immediately
    // if we run the whole test suite, because running the whole
    // test suite don't do any immediate recursive loops. Thus,
    // allowing a JS runtime to breathe.
    if (self._grep !== self._defaultGrep) {
      Runner.immediately(next);
    } else {
      next();
    }
    return;
  }

  if (test.isPending()) {
    if (self.forbidPending) {
      test.isPending = alwaysFalse;
      self.fail(test, new Error('Pending test forbidden'));
      delete test.isPending;
    } else {
      self.emit(constants.EVENT_TEST_PENDING, test);
    }
    self.emit(constants.EVENT_TEST_END, test);
    return next();
  }

  // execute test and hook(s)
  self.emit(constants.EVENT_TEST_BEGIN, (self.test = test));
  self.hookDown(HOOK_TYPE_BEFORE_EACH, function(err, errSuite) {
    if (test.isPending()) {
      if (self.forbidPending) {
        test.isPending = alwaysFalse;
        self.fail(test, new Error('Pending test forbidden'));
        delete test.isPending;
      } else {
        self.emit(constants.EVENT_TEST_PENDING, test);
      }
      self.emit(constants.EVENT_TEST_END, test);
      return next();
    }
    if (err) {
      return hookErr(err, errSuite, false);
    }
    self.currentRunnable = self.test;
    self.runTest(function(err) {
      test = self.test;
      if (err) {
        var retry = test.currentRetry();
        if (err instanceof Pending && self.forbidPending) {
          self.fail(test, new Error('Pending test forbidden'));
        } else if (err instanceof Pending) {
          test.pending = true;
          self.emit(constants.EVENT_TEST_PENDING, test);
        } else if (retry < test.retries()) {
          var clonedTest = test.clone();
          clonedTest.currentRetry(retry + 1);
          tests.unshift(clonedTest);

          self.emit(constants.EVENT_TEST_RETRY, test, err);

          // Early return + hook trigger so that it doesn't
          // increment the count wrong
          return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
        } else {
          self.fail(test, err);
        }
        self.emit(constants.EVENT_TEST_END, test);

        if (err instanceof Pending) {
          return next();
        }

        return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
      }

      test.state = STATE_PASSED;
      self.emit(constants.EVENT_TEST_PASS, test);
      self.emit(constants.EVENT_TEST_END, test);
      self.hookUp(HOOK_TYPE_AFTER_EACH, next);
    });
  });
}

this.next = next;
this.hookErr = hookErr;
next();

};

function alwaysFalse() {

return false;

}

/**

* Run the given `suite` and invoke the callback `fn()` when complete.
*
* @private
* @param {Suite} suite
* @param {Function} fn
*/

Runner.prototype.runSuite = function(suite, fn) {

var i = 0;
var self = this;
var total = this.grepTotal(suite);
var afterAllHookCalled = false;

debug('run suite %s', suite.fullTitle());

if (!total || (self.failures && suite._bail)) {
  return fn();
}

this.emit(constants.EVENT_SUITE_BEGIN, (this.suite = suite));

function next(errSuite) {
  if (errSuite) {
    // current suite failed on a hook from errSuite
    if (errSuite === suite) {
      // if errSuite is current suite
      // continue to the next sibling suite
      return done();
    }
    // errSuite is among the parents of current suite
    // stop execution of errSuite and all sub-suites
    return done(errSuite);
  }

  if (self._abort) {
    return done();
  }

  var curr = suite.suites[i++];
  if (!curr) {
    return done();
  }

  // Avoid grep neglecting large number of tests causing a
  // huge recursive loop and thus a maximum call stack error.
  // See comment in `this.runTests()` for more information.
  if (self._grep !== self._defaultGrep) {
    Runner.immediately(function() {
      self.runSuite(curr, next);
    });
  } else {
    self.runSuite(curr, next);
  }
}

function done(errSuite) {
  self.suite = suite;
  self.nextSuite = next;

  if (afterAllHookCalled) {
    fn(errSuite);
  } else {
    // mark that the afterAll block has been called once
    // and so can be skipped if there is an error in it.
    afterAllHookCalled = true;

    // remove reference to test
    delete self.test;

    self.hook(HOOK_TYPE_AFTER_ALL, function() {
      self.emit(constants.EVENT_SUITE_END, suite);
      fn(errSuite);
    });
  }
}

this.nextSuite = next;

this.hook(HOOK_TYPE_BEFORE_ALL, function(err) {
  if (err) {
    return done();
  }
  self.runTests(suite, next);
});

};

/**

* Handle uncaught exceptions.
*
* @param {Error} err
* @private
*/

Runner.prototype.uncaught = function(err) {

if (err instanceof Pending) {
  return;
}
if (err) {
  debug('uncaught exception %O', err);
} else {
  debug('uncaught undefined/falsy exception');
  err = createInvalidExceptionError(
    'Caught falsy/undefined exception which would otherwise be uncaught. No stack trace found; try a debugger',
    err
  );
}

if (!isError(err)) {
  err = thrown2Error(err);
}
err.uncaught = true;

var runnable = this.currentRunnable;

if (!runnable) {
  runnable = new Runnable('Uncaught error outside test suite');
  runnable.parent = this.suite;

  if (this.started) {
    this.fail(runnable, err);
  } else {
    // Can't recover from this failure
    this.emit(constants.EVENT_RUN_BEGIN);
    this.fail(runnable, err);
    this.emit(constants.EVENT_RUN_END);
  }

  return;
}

runnable.clearTimeout();

// Ignore errors if already failed or pending
// See #3226
if (runnable.isFailed() || runnable.isPending()) {
  return;
}
// we cannot recover gracefully if a Runnable has already passed
// then fails asynchronously
var alreadyPassed = runnable.isPassed();
// this will change the state to "failed" regardless of the current value
this.fail(runnable, err);
if (!alreadyPassed) {
  // recover from test
  if (runnable.type === constants.EVENT_TEST_BEGIN) {
    this.emit(constants.EVENT_TEST_END, runnable);
    this.hookUp(HOOK_TYPE_AFTER_EACH, this.next);
    return;
  }
  debug(runnable);

  // recover from hooks
  var errSuite = this.suite;

  // XXX how about a less awful way to determine this?
  // if hook failure is in afterEach block
  if (runnable.fullTitle().indexOf('after each') > -1) {
    return this.hookErr(err, errSuite, true);
  }
  // if hook failure is in beforeEach block
  if (runnable.fullTitle().indexOf('before each') > -1) {
    return this.hookErr(err, errSuite, false);
  }
  // if hook failure is in after or before blocks
  return this.nextSuite(errSuite);
}

// bail
this.emit(constants.EVENT_RUN_END);

};

/**

* Run the root suite and invoke `fn(failures)`
* on completion.
*
* @public
* @memberof Runner
* @param {Function} fn
* @return {Runner} Runner instance.
*/

Runner.prototype.run = function(fn) {

var self = this;
var rootSuite = this.suite;

fn = fn || function() {};

function uncaught(err) {
  self.uncaught(err);
}

function start() {
  // If there is an `only` filter
  if (rootSuite.hasOnly()) {
    rootSuite.filterOnly();
  }
  self.started = true;
  if (self._delay) {
    self.emit(constants.EVENT_DELAY_END);
  }
  self.emit(constants.EVENT_RUN_BEGIN);

  self.runSuite(rootSuite, function() {
    debug('finished running');
    self.emit(constants.EVENT_RUN_END);
  });
}

debug(constants.EVENT_RUN_BEGIN);

// references cleanup to avoid memory leaks
this.on(constants.EVENT_SUITE_END, function(suite) {
  suite.cleanReferences();
});

// callback
this.on(constants.EVENT_RUN_END, function() {
  debug(constants.EVENT_RUN_END);
  process.removeListener('uncaughtException', uncaught);
  fn(self.failures);
});

// uncaught exception
process.on('uncaughtException', uncaught);

if (this._delay) {
  // for reporters, I guess.
  // might be nice to debounce some dots while we wait.
  this.emit(constants.EVENT_DELAY_BEGIN, rootSuite);
  rootSuite.once(EVENT_ROOT_SUITE_RUN, start);
} else {
  start();
}

return this;

};

/**

* Cleanly abort execution.
*
* @memberof Runner
* @public
* @return {Runner} Runner instance.
*/

Runner.prototype.abort = function() {

debug('aborting');
this._abort = true;

return this;

};

/**

* Filter leaks with the given globals flagged as `ok`.
*
* @private
* @param {Array} ok
* @param {Array} globals
* @return {Array}
*/

function filterLeaks(ok, globals) {

return globals.filter(function(key) {
  // Firefox and Chrome exposes iframes as index inside the window object
  if (/^\d+/.test(key)) {
    return false;
  }

  // in firefox
  // if runner runs in an iframe, this iframe's window.getInterface method
  // not init at first it is assigned in some seconds
  if (global.navigator && /^getInterface/.test(key)) {
    return false;
  }

  // an iframe could be approached by window[iframeIndex]
  // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak
  if (global.navigator && /^\d+/.test(key)) {
    return false;
  }

  // Opera and IE expose global variables for HTML element IDs (issue #243)
  if (/^mocha-/.test(key)) {
    return false;
  }

  var matched = ok.filter(function(ok) {
    if (~ok.indexOf('*')) {
      return key.indexOf(ok.split('*')[0]) === 0;
    }
    return key === ok;
  });
  return !matched.length && (!global.navigator || key !== 'onerror');
});

}

/**

* Check if argument is an instance of Error object or a duck-typed equivalent.
*
* @private
* @param {Object} err - object to check
* @param {string} err.message - error message
* @returns {boolean}
*/

function isError(err) {

return err instanceof Error || (err && typeof err.message === 'string');

}

/**

*
* Converts thrown non-extensible type into proper Error.
*
* @private
* @param {*} thrown - Non-extensible type thrown by code
* @return {Error}
*/

function thrown2Error(err) {

return new Error(
  'the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)'
);

}

Runner.constants = constants;

/**

* Node.js' `EventEmitter`
* @external EventEmitter
* @see {@link https://nodejs.org/api/events.html#events_class_eventemitter}
*/