text: 'use strict';
  
  /**
   * Module dependencies.
   */
  
  var EventEmitter = require('events').EventEmitter;
  var JSON = require('json3');
  var Pending = require('./pending');
  var debug = require('debug')('mocha:runnable');
  var milliseconds = require('./ms');
  var utils = require('./utils');
  var create = require('lodash.create');
  
  /**
   * Save timer references to avoid Sinon interfering (see GH-237).
   */
  
  /* eslint-disable no-unused-vars, no-native-reassign */
  var Date = global.Date;
  var setTimeout = global.setTimeout;
  var setInterval = global.setInterval;
  var clearTimeout = global.clearTimeout;
  var clearInterval = global.clearInterval;
  /* eslint-enable no-unused-vars, no-native-reassign */
  
  /**
   * Object#toString().
   */
  
  var toString = Object.prototype.toString;
  
  /**
   * Expose `Runnable`.
   */
  
  module.exports = Runnable;
  
  /**
   * Initialize a new `Runnable` with the given `title` and callback `fn`.
   *
   * @param {String} title
   * @param {Function} fn
   * @api private
   * @param {string} title
   * @param {Function} fn
   */
  function Runnable (title, fn) {
    this.title = title;
    this.fn = fn;
    this.body = (fn || '').toString();
    this.async = fn && fn.length;
    this.sync = !this.async;
    this._timeout = 2000;
    this._slow = 75;
    this._enableTimeouts = true;
    this.timedOut = false;
    this._trace = new Error('done() called multiple times');
    this._retries = -1;
    this._currentRetry = 0;
    this.pending = false;
  }
  
  /**
   * Inherit from `EventEmitter.prototype`.
   */
  Runnable.prototype = create(EventEmitter.prototype, {
    constructor: Runnable
  });
  
  /**
   * Set & get timeout `ms`.
   *
   * @api private
   * @param {number|string} ms
   * @return {Runnable|number} ms or Runnable instance.
   */
  Runnable.prototype.timeout = function (ms) {
    if (!arguments.length) {
      return this._timeout;
    }
    // see #1652 for reasoning
    if (ms === 0 || ms > Math.pow(2, 31)) {
      this._enableTimeouts = false;
    }
    if (typeof ms === 'string') {
      ms = milliseconds(ms);
    }
    debug('timeout %d', ms);
    this._timeout = ms;
    if (this.timer) {
      this.resetTimeout();
    }
    return this;
  };
  
  /**
   * Set & get slow `ms`.
   *
   * @api private
   * @param {number|string} ms
   * @return {Runnable|number} ms or Runnable instance.
   */
  Runnable.prototype.slow = function (ms) {
    if (typeof ms === 'undefined') {
      return this._slow;
    }
    if (typeof ms === 'string') {
      ms = milliseconds(ms);
    }
    debug('timeout %d', ms);
    this._slow = ms;
    return this;
  };
  
  /**
   * Set and get whether timeout is `enabled`.
   *
   * @api private
   * @param {boolean} enabled
   * @return {Runnable|boolean} enabled or Runnable instance.
   */
  Runnable.prototype.enableTimeouts = function (enabled) {
    if (!arguments.length) {
      return this._enableTimeouts;
    }
    debug('enableTimeouts %s', enabled);
    this._enableTimeouts = enabled;
    return this;
  };
  
  /**
   * Halt and mark as pending.
   *
   * @api public
   */
  Runnable.prototype.skip = function () {
    throw new Pending('sync skip');
  };
  
  /**
   * Check if this runnable or its parent suite is marked as pending.
   *
   * @api private
   */
  Runnable.prototype.isPending = function () {
    return this.pending || (this.parent && this.parent.isPending());
  };
  
  /**
   * Set number of retries.
   *
   * @api private
   */
  Runnable.prototype.retries = function (n) {
    if (!arguments.length) {
      return this._retries;
    }
    this._retries = n;
  };
  
  /**
   * Get current retry
   *
   * @api private
   */
  Runnable.prototype.currentRetry = function (n) {
    if (!arguments.length) {
      return this._currentRetry;
    }
    this._currentRetry = n;
  };
  
  /**
   * Return the full title generated by recursively concatenating the parent's
   * full title.
   *
   * @api public
   * @return {string}
   */
  Runnable.prototype.fullTitle = function () {
    return this.parent.fullTitle() + ' ' + this.title;
  };
  
  /**
   * Clear the timeout.
   *
   * @api private
   */
  Runnable.prototype.clearTimeout = function () {
    clearTimeout(this.timer);
  };
  
  /**
   * Inspect the runnable void of private properties.
   *
   * @api private
   * @return {string}
   */
  Runnable.prototype.inspect = function () {
    return JSON.stringify(this, function (key, val) {
      if (key[0] === '_') {
        return;
      }
      if (key === 'parent') {
        return '#<Suite>';
      }
      if (key === 'ctx') {
        return '#<Context>';
      }
      return val;
    }, 2);
  };
  
  /**
   * Reset the timeout.
   *
   * @api private
   */
  Runnable.prototype.resetTimeout = function () {
    var self = this;
    var ms = this.timeout() || 1e9;
  
    if (!this._enableTimeouts) {
      return;
    }
    this.clearTimeout();
    this.timer = setTimeout(function () {
      if (!self._enableTimeouts) {
        return;
      }
      self.callback(new Error('Timeout of ' + ms +
        'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'));
      self.timedOut = true;
    }, ms);
  };
  
  /**
   * Whitelist a list of globals for this test run.
   *
   * @api private
   * @param {string[]} globals
   */
  Runnable.prototype.globals = function (globals) {
    if (!arguments.length) {
      return this._allowedGlobals;
    }
    this._allowedGlobals = globals;
  };
  
  /**
   * Run the test and invoke `fn(err)`.
   *
   * @param {Function} fn
   * @api private
   */
  Runnable.prototype.run = function (fn) {
    var self = this;
    var start = new Date();
    var ctx = this.ctx;
    var finished;
    var emitted;
  
    // Sometimes the ctx exists, but it is not runnable
    if (ctx && ctx.runnable) {
      ctx.runnable(this);
    }
  
    // called multiple times
    function multiple (err) {
      if (emitted) {
        return;
      }
      emitted = true;
      self.emit('error', err || new Error('done() called multiple times; stacktrace may be inaccurate'));
    }
  
    // finished
    function done (err) {
      var ms = self.timeout();
      if (self.timedOut) {
        return;
      }
      if (finished) {
        return multiple(err || self._trace);
      }
  
      self.clearTimeout();
      self.duration = new Date() - start;
      finished = true;
      if (!err && self.duration > ms && self._enableTimeouts) {
        err = new Error('Timeout of ' + ms +
        'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.');
      }
      fn(err);
    }
  
    // for .resetTimeout()
    this.callback = done;
  
    // explicit async with `done` argument
    if (this.async) {
      this.resetTimeout();
  
      // allows skip() to be used in an explicit async context
      this.skip = function asyncSkip () {
        done(new Pending('async skip call'));
        // halt execution.  the Runnable will be marked pending
        // by the previous call, and the uncaught handler will ignore
        // the failure.
        throw new Pending('async skip; aborting execution');
      };
  
      if (this.allowUncaught) {
        return callFnAsync(this.fn);
      }
      try {
        callFnAsync(this.fn);
      } catch (err) {
        emitted = true;
        done(utils.getError(err));
      }
      return;
    }
  
    if (this.allowUncaught) {
      callFn(this.fn);
      done();
      return;
    }
  
    // sync or promise-returning
    try {
      if (this.isPending()) {
        done();
      } else {
        callFn(this.fn);
      }
    } catch (err) {
      emitted = true;
      done(utils.getError(err));
    }
  
    function callFn (fn) {
      var result = fn.call(ctx);
      if (result && typeof result.then === 'function') {
        self.resetTimeout();
        result
          .then(function () {
            done();
            // Return null so libraries like bluebird do not warn about
            // subsequently constructed Promises.
            return null;
          },
          function (reason) {
            done(reason || new Error('Promise rejected with no or falsy reason'));
          });
      } else {
        if (self.asyncOnly) {
          return done(new Error('--async-only option in use without declaring `done()` or returning a promise'));
        }
  
        done();
      }
    }
  
    function callFnAsync (fn) {
      var result = fn.call(ctx, function (err) {
        if (err instanceof Error || toString.call(err) === '[object Error]') {
          return done(err);
        }
        if (err) {
          if (Object.prototype.toString.call(err) === '[object Object]') {
            return done(new Error('done() invoked with non-Error: ' +
              JSON.stringify(err)));
          }
          return done(new Error('done() invoked with non-Error: ' + err));
        }
        if (result && utils.isPromise(result)) {
          return done(new Error('Resolution method is overspecified. Specify a callback *or* return a Promise; not both.'));
        }
  
        done();
      });
    }
  };
  