Skip to content

Commit

Permalink
Add ability to run tests in a mocha instance multiple times (#4234); c…
Browse files Browse the repository at this point in the history
…loses #2783

* Add ability to run tests in a mocha instance multiple times

* Rename `autoDispsoe` to `cleanReferencesAfterRun`,

Rename since we cannot dispose the entire mocha instance after a test run. We should keep the process.on('uncaughtException') handlers in place in order to not break existing functionality.

* Allow `unloadFiles` to reset `referencesCleaned`.

* Complete rename of _cleanReferencesAfterRun

* Add integration test for running a suite multiple times

* improve api docs

* Docs: fix dead link

* Make sure tests run on older node versions

* Remove `.only` 😅

* Implement `process.listenerCount` in the browser

* Implement mocha states in a finite state machine

* Fix some small remarks

* Make integration tests more damp

* Keep `Runner` api backward compatible

* Unload files when disposed

* Runnable.reset should also reset `err` and `state`

* Also reset hooks

Co-authored-by: Christopher Hiller <boneskull@boneskull.com>
  • Loading branch information
nicojs and boneskull committed May 11, 2020
1 parent c0137eb commit fbe3ce4
Show file tree
Hide file tree
Showing 24 changed files with 853 additions and 26 deletions.
11 changes: 11 additions & 0 deletions browser-entry.js
Expand Up @@ -52,6 +52,17 @@ process.removeListener = function(e, fn) {
}
};

/**
* Implements listenerCount for 'uncaughtException'.
*/

process.listenerCount = function(name) {
if (name === 'uncaughtException') {
return uncaughtExceptionHandlers.length;
}
return 0;
};

/**
* Implements uncaughtException listener.
*/
Expand Down
33 changes: 32 additions & 1 deletion lib/errors.js
Expand Up @@ -149,6 +149,35 @@ function createInvalidPluginError(message, pluginType, pluginId) {
}
}

/**
* Creates an error object to be thrown when a mocha object's `run` method is executed while it is already disposed.
* @param {string} message The error message to be displayed.
* @param {boolean} cleanReferencesAfterRun the value of `cleanReferencesAfterRun`
* @param {Mocha} instance the mocha instance that throw this error
*/
function createMochaInstanceAlreadyDisposedError(
message,
cleanReferencesAfterRun,
instance
) {
var err = new Error(message);
err.code = 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED';
err.cleanReferencesAfterRun = cleanReferencesAfterRun;
err.instance = instance;
return err;
}

/**
* Creates an error object to be thrown when a mocha object's `run` method is called while a test run is in progress.
* @param {string} message The error message to be displayed.
*/
function createMochaInstanceAlreadyRunningError(message, instance) {
var err = new Error(message);
err.code = 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING';
err.instance = instance;
return err;
}

module.exports = {
createInvalidArgumentTypeError: createInvalidArgumentTypeError,
createInvalidArgumentValueError: createInvalidArgumentValueError,
Expand All @@ -158,5 +187,7 @@ module.exports = {
createMissingArgumentError: createMissingArgumentError,
createNoFilesMatchPatternError: createNoFilesMatchPatternError,
createUnsupportedError: createUnsupportedError,
createInvalidPluginError: createInvalidPluginError
createInvalidPluginError: createInvalidPluginError,
createMochaInstanceAlreadyDisposedError: createMochaInstanceAlreadyDisposedError,
createMochaInstanceAlreadyRunningError: createMochaInstanceAlreadyRunningError
};
8 changes: 8 additions & 0 deletions lib/hook.js
Expand Up @@ -27,6 +27,14 @@ function Hook(title, fn) {
*/
inherits(Hook, Runnable);

/**
* Resets the state for a next run.
*/
Hook.prototype.reset = function() {
Runnable.prototype.reset.call(this);
delete this._error;
};

/**
* Get or set the test `err`.
*
Expand Down
110 changes: 109 additions & 1 deletion lib/mocha.js
Expand Up @@ -18,13 +18,41 @@ var esmUtils = utils.supportsEsModules() ? require('./esm-utils') : undefined;
var createStatsCollector = require('./stats-collector');
var createInvalidReporterError = errors.createInvalidReporterError;
var createInvalidInterfaceError = errors.createInvalidInterfaceError;
var createMochaInstanceAlreadyDisposedError =
errors.createMochaInstanceAlreadyDisposedError;
var createMochaInstanceAlreadyRunningError =
errors.createMochaInstanceAlreadyRunningError;
var EVENT_FILE_PRE_REQUIRE = Suite.constants.EVENT_FILE_PRE_REQUIRE;
var EVENT_FILE_POST_REQUIRE = Suite.constants.EVENT_FILE_POST_REQUIRE;
var EVENT_FILE_REQUIRE = Suite.constants.EVENT_FILE_REQUIRE;
var sQuote = utils.sQuote;

exports = module.exports = Mocha;

/**
* A Mocha instance is a finite state machine.
* These are the states it can be in.
*/
var mochaStates = utils.defineConstants({
/**
* Initial state of the mocha instance
*/
INIT: 'init',
/**
* Mocha instance is running tests
*/
RUNNING: 'running',
/**
* Mocha instance is done running tests and references to test functions and hooks are cleaned.
* You can reset this state by unloading the test files.
*/
REFERENCES_CLEANED: 'referencesCleaned',
/**
* Mocha instance is disposed and can no longer be used.
*/
DISPOSED: 'disposed'
});

/**
* To require local UIs and reporters when running in node.
*/
Expand Down Expand Up @@ -97,6 +125,7 @@ function Mocha(options) {
this.options = options;
// root suite
this.suite = new exports.Suite('', new exports.Context(), true);
this._cleanReferencesAfterRun = true;

this.grep(options.grep)
.fgrep(options.fgrep)
Expand Down Expand Up @@ -388,9 +417,18 @@ Mocha.unloadFile = function(file) {
* @chainable
*/
Mocha.prototype.unloadFiles = function() {
if (this._state === mochaStates.DISPOSED) {
throw createMochaInstanceAlreadyDisposedError(
'Mocha instance is already disposed, it cannot be used again.',
this._cleanReferencesAfterRun,
this
);
}

this.files.forEach(function(file) {
Mocha.unloadFile(file);
});
this._state = mochaStates.INIT;
return this;
};

Expand Down Expand Up @@ -490,6 +528,38 @@ Mocha.prototype.checkLeaks = function(checkLeaks) {
return this;
};

/**
* Enables or disables whether or not to dispose after each test run.
* Disable this to ensure you can run the test suite multiple times.
* If disabled, be sure to dispose mocha when you're done to prevent memory leaks.
* @public
* @see {@link Mocha#dispose}
* @param {boolean} cleanReferencesAfterRun
* @return {Mocha} this
* @chainable
*/
Mocha.prototype.cleanReferencesAfterRun = function(cleanReferencesAfterRun) {
this._cleanReferencesAfterRun = cleanReferencesAfterRun !== false;
return this;
};

/**
* Manually dispose this mocha instance. Mark this instance as `disposed` and unable to run more tests.
* It also removes function references to tests functions and hooks, so variables trapped in closures can be cleaned by the garbage collector.
* @public
*/
Mocha.prototype.dispose = function() {
if (this._state === mochaStates.RUNNING) {
throw createMochaInstanceAlreadyRunningError(
'Cannot dispose while the mocha instance is still running tests.'
);
}
this.unloadFiles();
this._previousRunner && this._previousRunner.dispose();
this.suite.dispose();
this._state = mochaStates.DISPOSED;
};

/**
* Displays full stack trace upon test failure.
*
Expand Down Expand Up @@ -770,6 +840,28 @@ Mocha.prototype.forbidPending = function(forbidPending) {
return this;
};

/**
* Throws an error if mocha is in the wrong state to be able to transition to a "running" state.
*/
Mocha.prototype._guardRunningStateTransition = function() {
if (this._state === mochaStates.RUNNING) {
throw createMochaInstanceAlreadyRunningError(
'Mocha instance is currently running tests, cannot start a next test run until this one is done',
this
);
}
if (
this._state === mochaStates.DISPOSED ||
this._state === mochaStates.REFERENCES_CLEANED
) {
throw createMochaInstanceAlreadyDisposedError(
'Mocha instance is already disposed, cannot start a new test run. Please create a new mocha instance. Be sure to set disable `cleanReferencesAfterRun` when you want to reuse the same mocha instance for multiple test runs.',
this._cleanReferencesAfterRun,
this
);
}
};

/**
* Mocha version as specified by "package.json".
*
Expand Down Expand Up @@ -810,13 +902,23 @@ Object.defineProperty(Mocha.prototype, 'version', {
* mocha.run(failures => process.exitCode = failures ? 1 : 0);
*/
Mocha.prototype.run = function(fn) {
this._guardRunningStateTransition();
this._state = mochaStates.RUNNING;
if (this._previousRunner) {
this._previousRunner.dispose();
this.suite.reset();
}
if (this.files.length && !this.loadAsync) {
this.loadFiles();
}
var self = this;
var suite = this.suite;
var options = this.options;
options.files = this.files;
var runner = new exports.Runner(suite, options.delay);
var runner = new exports.Runner(suite, {
delay: options.delay,
cleanReferencesAfterRun: this._cleanReferencesAfterRun
});
createStatsCollector(runner);
var reporter = new this._reporter(runner, options);
runner.checkLeaks = options.checkLeaks === true;
Expand All @@ -841,6 +943,12 @@ Mocha.prototype.run = function(fn) {
exports.reporters.Base.hideDiff = !options.diff;

function done(failures) {
self._previousRunner = runner;
if (self._cleanReferencesAfterRun) {
self._state = mochaStates.REFERENCES_CLEANED;
} else {
self._state = mochaStates.INIT;
}
fn = fn || utils.noop;
if (reporter.done) {
reporter.done(failures, fn);
Expand Down
15 changes: 12 additions & 3 deletions lib/runnable.js
Expand Up @@ -35,17 +35,26 @@ function Runnable(title, fn) {
this.sync = !this.async;
this._timeout = 2000;
this._slow = 75;
this.timedOut = false;
this._retries = -1;
this._currentRetry = 0;
this.pending = false;
this.reset();
}

/**
* Inherit from `EventEmitter.prototype`.
*/
utils.inherits(Runnable, EventEmitter);

/**
* Resets the state initially or for a next run.
*/
Runnable.prototype.reset = function() {
this.timedOut = false;
this._currentRetry = 0;
this.pending = false;
delete this.state;
delete this.err;
};

/**
* Get current timeout value in msecs.
*
Expand Down

0 comments on commit fbe3ce4

Please sign in to comment.