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

chore: fully clear require/import cache in non-parallel watch mode #5155

Closed
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/cli/handle-requires.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';
const fs = require('fs');
const path = require('path');
const debug = require('debug')('mocha:cli:handle:requires');
const {requireOrImport} = require('../nodejs/esm-utils');
const PluginLoader = require('../plugin-loader');

/**
* `require()` the modules as required by `--require <require>`.
*
* Returns array of `mochaHooks` exports, if any.
* @param {string[]} requires - Modules to require
* @returns {Promise<object>} Plugin implementations
* @private
*/
exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => {
const pluginLoader = PluginLoader.create({ignore: ignoredPlugins});
for await (const mod of requires) {
let modpath = mod;
// this is relative to cwd
if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
modpath = path.resolve(mod);
debug('resolved required file %s to %s', mod, modpath);
}
const requiredModule = await requireOrImport(modpath);
if (requiredModule && typeof requiredModule === 'object') {
if (pluginLoader.load(requiredModule)) {
debug('found one or more plugin implementations in %s', modpath);
}
}
debug('loaded required module "%s"', mod);
}
const plugins = await pluginLoader.finalize();
if (Object.keys(plugins).length) {
debug('finalized plugin implementations: %O', plugins);
}
return plugins;
};
35 changes: 0 additions & 35 deletions lib/cli/run-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@
* @private
*/

const fs = require('fs');
const path = require('path');
const debug = require('debug')('mocha:cli:run:helpers');
const {watchRun, watchParallelRun} = require('./watch-run');
const collectFiles = require('./collect-files');
const {format} = require('util');
const {createInvalidLegacyPluginError} = require('../errors');
const {requireOrImport} = require('../nodejs/esm-utils');
const PluginLoader = require('../plugin-loader');

/**
* Exits Mocha when tests + code under test has finished execution (default)
Expand Down Expand Up @@ -74,38 +71,6 @@ const exitMocha = code => {
exports.list = str =>
Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);

/**
* `require()` the modules as required by `--require <require>`.
*
* Returns array of `mochaHooks` exports, if any.
* @param {string[]} requires - Modules to require
* @returns {Promise<object>} Plugin implementations
* @private
*/
exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => {
const pluginLoader = PluginLoader.create({ignore: ignoredPlugins});
for await (const mod of requires) {
let modpath = mod;
// this is relative to cwd
if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
modpath = path.resolve(mod);
debug('resolved required file %s to %s', mod, modpath);
}
const requiredModule = await requireOrImport(modpath);
if (requiredModule && typeof requiredModule === 'object') {
if (pluginLoader.load(requiredModule)) {
debug('found one or more plugin implementations in %s', modpath);
}
}
debug('loaded required module "%s"', mod);
}
const plugins = await pluginLoader.finalize();
if (Object.keys(plugins).length) {
debug('finalized plugin implementations: %O', plugins);
}
return plugins;
};

/**
* Collect and load test files, then run mocha instance.
* @param {Mocha} mocha - Mocha instance
Expand Down
8 changes: 2 additions & 6 deletions lib/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ const {
createMissingArgumentError
} = require('../errors');

const {
list,
handleRequires,
validateLegacyPlugin,
runMocha
} = require('./run-helpers');
const {list, validateLegacyPlugin, runMocha} = require('./run-helpers');
const {handleRequires} = require('./handle-requires');
const {ONE_AND_DONES, ONE_AND_DONE_ARGS} = require('./one-and-dones');
const debug = require('debug')('mocha:cli:run');
const defaults = require('../mocharc');
Expand Down
45 changes: 34 additions & 11 deletions lib/cli/watch-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const path = require('path');
const chokidar = require('chokidar');
const Context = require('../context');
const collectFiles = require('./collect-files');

const {handleRequires} = require('./handle-requires');
const {setCacheBusterKey} = require('../nodejs/esm-utils');
const {uniqueID} = require('../utils');
/**
* Exports the `watchRun` function that runs mocha in "watch" mode.
* @see module:lib/cli/run-helpers
Expand Down Expand Up @@ -97,9 +99,14 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => {
return createWatcher(mocha, {
watchFiles,
watchIgnore,
beforeRun({mocha}) {
async beforeRun({mocha}) {
mocha.unloadFiles();

// Reload hooks. If not done, global hooks keep their state between watch runs, but test files always get a new
// instance, making state mutation impossible via global hooks.
const plugins = await handleRequires(mocha.options.require);
mocha.options.rootHooks = plugins.rootHooks;

// I don't know why we're cloning the root suite.
const rootSuite = mocha.suite.clone();

Expand Down Expand Up @@ -254,16 +261,21 @@ const createRerunner = (mocha, watcher, {beforeRun} = {}) => {
// running.
let runner = null;

const blastFullCache = !mocha.options.parallel;
Copy link
Member

Choose a reason for hiding this comment

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

An alternative is to always do a full blast, and then we can remove some conditional logic and simplify a bit.

Yeah this is a good question... I'd personally lean towards always blasting the full cache.

  • It's odd for both contributors and users having different behaviors between modes
  • Just in case, it'd be good to clamp down on this kind of issue whenever possible

...provided there isn't a noticeable performance hit. Have you observed any particular slowdowns trying this out?

We're still a swamped with picking the project up right now and other commitments, so we're not likely to be able to do a full performance dive anytime soon (#5027). If you can set that up to just get a quick baseline of whether this is bad, that'd be very helpful!

Note also that we wouldn't be able to merge this soon - we've got some more work to get through before we can start shipping semver-major changes.

Copy link
Author

Choose a reason for hiding this comment

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

About benchmarking: I ran it three times with the conditional blasting, and three times with the latest commit always blasting. It looks negligible, in both scenarios milliseconds go up and down a bit, but are in the same range. Around mid July I can do a more decent benchmark, with potentially a few hundreds of dependencies, if you wish.

Results (only test/integration/options/watch.spec.js):

Conditionally blasting:

      ✔ reruns test when watched test file is touched (4017ms)
      ✔ reruns test when watched test file crashes (4012ms)
      ✔ reruns test when file matching --watch-files changes (4016ms)
      ✔ reruns test when file matching --watch-files is added (4017ms)
      ✔ reruns test when file matching --watch-files is removed (4015ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4021ms)
      ✔ picks up new test files when they are added (4018ms)
      ✔ reruns test when file matching --extension is changed (4017ms)
      ✔ reruns when "rs\n" typed (4017ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4021ms)
      ✔ ignores files in "node_modules" and ".git" by default (4023ms)
      ✔ ignores files matching --watch-ignore (4023ms)
      ✔ reloads test files when they change (4021ms)
      ✔ reloads test dependencies when they change (4024ms)
      ✔ reloads all required dependencies between runs (4023ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4021ms)
      ✔ respects --fgrep on re-runs (4028ms)
      ✔ should not leak event listeners (15039ms)
        ✔ reruns test when watched test file is touched (4019ms)
        ✔ reruns test when watched test file is crashed (4018ms)
        ✔ mochaHooks.beforeAll runs as expected (4021ms)
        ✔ mochaHooks.beforeEach runs as expected (4024ms)
        ✔ mochaHooks.afterAll runs as expected (4026ms)
        ✔ mochaHooks.afterEach runs as expected (4022ms)

      ✔ reruns test when watched test file is touched (4029ms)
      ✔ reruns test when watched test file crashes (4018ms)
      ✔ reruns test when file matching --watch-files changes (4029ms)
      ✔ reruns test when file matching --watch-files is added (4030ms)
      ✔ reruns test when file matching --watch-files is removed (4034ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4022ms)
      ✔ picks up new test files when they are added (4029ms)
      ✔ reruns test when file matching --extension is changed (4016ms)
      ✔ reruns when "rs\n" typed (4013ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4028ms)
      ✔ ignores files in "node_modules" and ".git" by default (4028ms)
      ✔ ignores files matching --watch-ignore (4019ms)
      ✔ reloads test files when they change (4033ms)
      ✔ reloads test dependencies when they change (4039ms)
      ✔ reloads all required dependencies between runs (4033ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4026ms)
      ✔ respects --fgrep on re-runs (4037ms)
      ✔ should not leak event listeners (15053ms)
        ✔ reruns test when watched test file is touched (4024ms)
        ✔ reruns test when watched test file is crashed (4030ms)
        ✔ mochaHooks.beforeAll runs as expected (4028ms)
        ✔ mochaHooks.beforeEach runs as expected (4032ms)
        ✔ mochaHooks.afterAll runs as expected (4024ms)
        ✔ mochaHooks.afterEach runs as expected (4042ms)

      ✔ reruns test when watched test file is touched (4042ms)
      ✔ reruns test when watched test file crashes (4012ms)
      ✔ reruns test when file matching --watch-files changes (4017ms)
      ✔ reruns test when file matching --watch-files is added (4036ms)
      ✔ reruns test when file matching --watch-files is removed (4024ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4024ms)
      ✔ picks up new test files when they are added (4035ms)
      ✔ reruns test when file matching --extension is changed (4024ms)
      ✔ reruns when "rs\n" typed (4032ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4025ms)
      ✔ ignores files in "node_modules" and ".git" by default (4025ms)
      ✔ ignores files matching --watch-ignore (4025ms)
      ✔ reloads test files when they change (4032ms)
      ✔ reloads test dependencies when they change (4032ms)
      ✔ reloads all required dependencies between runs (4036ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4032ms)
      ✔ respects --fgrep on re-runs (4049ms)
      ✔ should not leak event listeners (15054ms)
      when in parallel mode
        ✔ reruns test when watched test file is touched (4026ms)
        ✔ reruns test when watched test file is crashed (4018ms)
      with required hooks
        ✔ mochaHooks.beforeAll runs as expected (4024ms)
        ✔ mochaHooks.beforeEach runs as expected (4026ms)
        ✔ mochaHooks.afterAll runs as expected (4025ms)
        ✔ mochaHooks.afterEach runs as expected (4028ms)

Always blasting:

      ✔ reruns test when watched test file is touched (4026ms)
      ✔ reruns test when watched test file crashes (4022ms)
      ✔ reruns test when file matching --watch-files changes (4024ms)
      ✔ reruns test when file matching --watch-files is added (4026ms)
      ✔ reruns test when file matching --watch-files is removed (4025ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4030ms)
      ✔ picks up new test files when they are added (4031ms)
      ✔ reruns test when file matching --extension is changed (4026ms)
      ✔ reruns when "rs\n" typed (4028ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4024ms)
      ✔ ignores files in "node_modules" and ".git" by default (4023ms)
      ✔ ignores files matching --watch-ignore (4025ms)
      ✔ reloads test files when they change (4028ms)
      ✔ reloads test dependencies when they change (4025ms)
      ✔ reloads all required dependencies between runs (4025ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4039ms)
      ✔ respects --fgrep on re-runs (4024ms)
      ✔ should not leak event listeners (15055ms)
        ✔ reruns test when watched test file is touched (4012ms)
        ✔ reruns test when watched test file is crashed (4026ms)
        ✔ mochaHooks.beforeAll runs as expected (4026ms)
        ✔ mochaHooks.beforeEach runs as expected (4025ms)
        ✔ mochaHooks.afterAll runs as expected (4024ms)
        ✔ mochaHooks.afterEach runs as expected (4026ms)

      ✔ reruns test when watched test file is touched (4027ms)
      ✔ reruns test when watched test file crashes (4024ms)
      ✔ reruns test when file matching --watch-files changes (4025ms)
      ✔ reruns test when file matching --watch-files is added (4028ms)
      ✔ reruns test when file matching --watch-files is removed (4025ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4023ms)
      ✔ picks up new test files when they are added (4031ms)
      ✔ reruns test when file matching --extension is changed (4025ms)
      ✔ reruns when "rs\n" typed (4027ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4024ms)
      ✔ ignores files in "node_modules" and ".git" by default (4026ms)
      ✔ ignores files matching --watch-ignore (4024ms)
      ✔ reloads test files when they change (4025ms)
      ✔ reloads test dependencies when they change (4028ms)
      ✔ reloads all required dependencies between runs (4025ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4025ms)
      ✔ respects --fgrep on re-runs (4031ms)
      ✔ should not leak event listeners (15038ms)
        ✔ reruns test when watched test file is touched (4021ms)
        ✔ reruns test when watched test file is crashed (4026ms)
        ✔ mochaHooks.beforeAll runs as expected (4023ms)
        ✔ mochaHooks.beforeEach runs as expected (4026ms)
        ✔ mochaHooks.afterAll runs as expected (4025ms)
        ✔ mochaHooks.afterEach runs as expected (4023ms)

      ✔ reruns test when watched test file is touched (4018ms)
      ✔ reruns test when watched test file crashes (4018ms)
      ✔ reruns test when file matching --watch-files changes (4014ms)
      ✔ reruns test when file matching --watch-files is added (4015ms)
      ✔ reruns test when file matching --watch-files is removed (4016ms)
      ✔ does not rerun test when file not matching --watch-files is changed (4021ms)
      ✔ picks up new test files when they are added (4021ms)
      ✔ reruns test when file matching --extension is changed (4028ms)
      ✔ reruns when "rs\n" typed (4031ms)
      ✔ reruns test when file starting with . and matching --extension is changed (4025ms)
      ✔ ignores files in "node_modules" and ".git" by default (4025ms)
      ✔ ignores files matching --watch-ignore (4023ms)
      ✔ reloads test files when they change (4029ms)
      ✔ reloads test dependencies when they change (4025ms)
      ✔ reloads all required dependencies between runs (4017ms)
      ✔ reloads all required dependencies between runs when mutated from a hook (4028ms)
      ✔ respects --fgrep on re-runs (4038ms)
      ✔ should not leak event listeners (15049ms)
        ✔ reruns test when watched test file is touched (4023ms)
        ✔ reruns test when watched test file is crashed (4025ms)
        ✔ mochaHooks.beforeAll runs as expected (4032ms)
        ✔ mochaHooks.beforeEach runs as expected (4027ms)
        ✔ mochaHooks.afterAll runs as expected (4025ms)
        ✔ mochaHooks.afterEach runs as expected (4025ms)

Copy link
Author

Choose a reason for hiding this comment

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

About full-blast always: I think it is the best approach, but I wanted to both play it safe and gather your feedback first. Removed the conditional logic and now always doing a full cache blast. Let me know if you see anything to improve

About when to release a new major: No problem, there's no hurry, and worst case scenario could just use my fork's branch.

Thanks for keeping the library alive and maintained!

Copy link
Member

Choose a reason for hiding this comment

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

Great, sounds good to me! Full cache blasting is a 👍 from me. 🙂


// true if a file has changed during a test run
let rerunScheduled = false;

const run = () => {
const run = async () => {
try {
mocha = beforeRun ? beforeRun({mocha, watcher}) || mocha : mocha;
mocha = beforeRun ? (await beforeRun({mocha, watcher})) || mocha : mocha;
runner = mocha.run(() => {
debug('finished watch run');
runner = null;
blastCache(watcher);
if (blastFullCache) {
setCacheBusterKey(uniqueID());
}
blastCache(watcher, blastFullCache);
if (rerunScheduled) {
rerun();
} else {
Expand Down Expand Up @@ -348,15 +360,26 @@ const eraseLine = () => {
/**
* Blast all of the watched files out of `require.cache`
* @param {FSWatcher} watcher - chokidar FSWatcher
* @param {boolean} blastAll
* @ignore
* @private
*/
const blastCache = watcher => {
const files = getWatchedFiles(watcher);
files.forEach(file => {
delete require.cache[file];
});
debug('deleted %d file(s) from the require cache', files.length);
const blastCache = (watcher, blastAll) => {
if (blastAll) {
Object.keys(require.cache)
// Avoid deleting mocha binary (at minimum, breaks Mocha's watch tests)
.filter(file => !file.includes('/mocha/bin/'))
Copy link
Member

Choose a reason for hiding this comment

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

[Bug] What if someone has a structure like ./src/confusing/mocha/bin/hooks.js? There's nothing stopping folks from doing that and then reporting it as a bug. This'll need to be a more specific filter.

Copy link
Author

Choose a reason for hiding this comment

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

100% Agree. Narrowed the filter to the binary both when running Mocha's tests (../bin/..) and when running as an installed dependency ../lib/..).

.forEach(file => {
delete require.cache[file];
});
debug('deleted all files from the require cache');
} else {
const files = getWatchedFiles(watcher);
files.forEach(file => {
delete require.cache[file];
});
debug('deleted %d file(s) from the require cache', files.length);
}
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
};

/**
Expand Down
28 changes: 26 additions & 2 deletions lib/nodejs/esm-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,34 @@ const url = require('url');

const forward = x => x;

let cacheBusterKey = '';

// When changing the key, old items won't be garbage collected, but there is no alternative with import().
// More info: https://github.com/nodejs/node/issues/49442
const withCacheBuster = x => {
if (!cacheBusterKey) {
return x;
}
if (typeof x === 'string') {
return x.includes('?')
? `${x}&cache=${cacheBusterKey}`
: `${x}?cache=${cacheBusterKey}`;
} else if (x instanceof url.URL) {
x.searchParams.append('cache', cacheBusterKey);
}
return x;
};

exports.setCacheBusterKey = key => {
cacheBusterKey = key;
};

const formattedImport = async (file, esmDecorator = forward) => {
if (path.isAbsolute(file)) {
try {
return await exports.doImport(esmDecorator(url.pathToFileURL(file)));
return await exports.doImport(
withCacheBuster(esmDecorator(url.pathToFileURL(file)))
);
} catch (err) {
// This is a hack created because ESM in Node.js (at least in Node v15.5.1) does not emit
// the location of the syntax error in the error thrown.
Expand All @@ -29,7 +53,7 @@ const formattedImport = async (file, esmDecorator = forward) => {
throw err;
}
}
return exports.doImport(esmDecorator(file));
return exports.doImport(withCacheBuster(esmDecorator(file)));
};

exports.doImport = async file => import(file);
Expand Down
3 changes: 2 additions & 1 deletion lib/nodejs/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const {
} = require('../errors');
const workerpool = require('workerpool');
const Mocha = require('../mocha');
const {handleRequires, validateLegacyPlugin} = require('../cli/run-helpers');
const {validateLegacyPlugin} = require('../cli/run-helpers');
const {handleRequires} = require('../cli/handle-requires');
const d = require('debug');
const debug = d.debug(`mocha:parallel:worker:${process.pid}`);
const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
let flag = false;

module.exports.getFlag = () => flag;

module.exports.enableFlag = () => {
flag = true;
};

module.exports.disableFlag = () => {
flag = false;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const dependency = require('./lib/dependency-with-state');

module.exports = {
mochaHooks: {
beforeEach: () => {
dependency.enableFlag();
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const dependency = require('./lib/dependency-with-state');

it('checks and mutates dependency', () => {
if (dependency.getFlag()) {
throw new Error('test failed');
}
// Will pass 1st run, fail on subsequent ones
dependency.enableFlag();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const dependency = require('./lib/dependency-with-state');

// Will fail 1st run, unless hook runs
before(() => {
dependency.disableFlag();
});

// Will pass 1st run, fail on subsequent ones, unless hook runs
afterEach(() => {
dependency.disableFlag();
});

it('hook should have mutated dependency', () => {
if (!dependency.getFlag()) {
throw new Error('test failed');
}
});
65 changes: 65 additions & 0 deletions test/integration/options/watch.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,71 @@ describe('--watch', function () {
});
});

// Regression test for https://github.com/mochajs/mocha/issues/5149
it('reloads all required dependencies between runs', function () {
const testFile = path.join(tempDir, 'test-mutating-dependency.js');
copyFixture('options/watch/test-mutating-dependency', testFile);

const dependency = path.join(tempDir, 'lib', 'dependency-with-state.js');
copyFixture('options/watch/dependency-with-state', dependency);

// Notice we are watching only the test file, skipping the dependency file.
// This is a simplification of a scenario where there's an unwatched file somewhere in the dependency tree
// that is mutated between runs, and not properly reset.
return runMochaWatchJSONAsync(
[testFile, '--watch-files', 'test-mutating-dependency.js'],
tempDir,
() => {
replaceFileContents(
testFile,
'// Will pass 1st run, fail on subsequent ones',
'// Will pass 1st run, fail on subsequent runs'
);
}
).then(results => {
expect(results, 'to have length', 2);
expect(results[0].passes, 'to have length', 1);
expect(results[0].failures, 'to have length', 0);
expect(results[1].passes, 'to have length', 1);
expect(results[1].failures, 'to have length', 0);
});
});

// Regression test for https://github.com/mochajs/mocha/issues/5149
it('reloads all required dependencies between runs when mutated from a hook', function () {
const testFile = path.join(
tempDir,
'test-with-hook-mutated-dependency.js'
);
copyFixture('options/watch/test-with-hook-mutated-dependency', testFile);

const dependency = path.join(tempDir, 'lib', 'dependency-with-state.js');
copyFixture('options/watch/dependency-with-state', dependency);

const hookFile = path.join(tempDir, 'hook-mutating-dependency.js');
copyFixture('options/watch/hook-mutating-dependency', hookFile);

return runMochaWatchJSONAsync(
[
testFile,
'--require',
hookFile,
'--watch-files',
'test-with-hook-mutated-dependency.js'
],
tempDir,
() => {
touchFile(testFile);
}
).then(results => {
expect(results.length, 'to equal', 2);
expect(results[0].passes, 'to have length', 1);
expect(results[0].failures, 'to have length', 0);
expect(results[1].passes, 'to have length', 1);
expect(results[1].failures, 'to have length', 0);
});
});

// Regression test for https://github.com/mochajs/mocha/issues/2027
it('respects --fgrep on re-runs', async function () {
const testFile = path.join(tempDir, 'test.js');
Expand Down
Loading
Loading