Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to run tests in a mocha instance multiple times #4234

Merged
merged 19 commits into from May 11, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: this is an observation and not a request for changes.

I don't love how process is shimmed here. we might be able to get away with changing the prototype to the EventEmitter shim (loaded via browserify via events, because process should be an EventEmitter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was surprised that that wasn't yet the case.

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;
boneskull marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -787,6 +857,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 @@ -827,13 +919,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 @@ -858,6 +960,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 @@ -36,17 +36,26 @@ function Runnable(title, fn) {
this._timeout = 2000;
this._slow = 75;
this._enableTimeouts = true;
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