Skip to content

Commit ad3205b

Browse files
committed
Revert "fix: begin running tests immediately instead of waiting for watcher, reload required files in watch mode (#5409)"
This reverts commit 7d9ade6.
1 parent 7d9ade6 commit ad3205b

10 files changed

Lines changed: 139 additions & 797 deletions

File tree

.knip.jsonc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
"project": ["{bin,lib,scripts,test}/**/*.{js,ts,mjs,cjs}"],
1212
"ignore": [
1313
"test/integration/fixtures/esm/type-module/test-that-imports-non-existing-module.fixture.js",
14-
"test/integration/fixtures/options/watch/mock-global-setup-with-dependency.fixture.js",
1514
"test/integration/fixtures/options/watch/test-with-dependency.fixture.js",
16-
"test/integration/fixtures/options/watch/test-with-dependency-and-barrier.fixture.js",
1715
],
1816
"ignoreDependencies": [
1917
"@test/esm-only-loader",

lib/cli/run-helpers.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,16 @@ exports.list = (str) =>
8787
Array.isArray(str) ? exports.list(str.join(",")) : str.split(/ *, */);
8888

8989
/**
90-
* `import()` or `require()` the modules as required by `--require <require>`.
90+
* `require()` the modules as required by `--require <require>`.
9191
*
9292
* Returns array of `mochaHooks` exports, if any.
9393
* @param {string[]} requires - Modules to require
94-
* @param {Object} [opts] - additional options
95-
* @param {string[]} [opts.ignoredPlugins] - ignored plugins by export name
96-
* @param {boolean} [opts.forceRequire] - if true, don't try to `import()`
97-
* the modules. Watch serial mode uses this to ensure root hook modules
98-
* get `require()`d, because if `import()`ed there's no way to reload them
99-
* after they've changed.
10094
* @returns {Promise<object>} Plugin implementations
10195
* @private
10296
*/
10397
exports.handleRequires = async (
10498
requires = [],
105-
{ ignoredPlugins = [], forceRequire } = {},
99+
{ ignoredPlugins = [] } = {},
106100
) => {
107101
const pluginLoader = PluginLoader.create({ ignore: ignoredPlugins });
108102
for await (const mod of requires) {
@@ -112,9 +106,7 @@ exports.handleRequires = async (
112106
modpath = path.resolve(mod);
113107
debug("resolved required file %s to %s", mod, modpath);
114108
}
115-
const requiredModule = forceRequire
116-
? require(modpath)
117-
: await requireOrImport(modpath);
109+
const requiredModule = await requireOrImport(modpath);
118110
if (requiredModule && typeof requiredModule === "object") {
119111
if (pluginLoader.load(requiredModule)) {
120112
debug("found one or more plugin implementations in %s", modpath);

lib/cli/watch-run.js

Lines changed: 63 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"use strict";
22

3-
const { clearTimeout, setTimeout } = global;
4-
53
const debug = require("debug")("mocha:cli:watch");
64
const path = require("node:path");
75
const chokidar = require("chokidar");
@@ -10,7 +8,6 @@ const isPathInside = require("is-path-inside");
108
const { minimatch } = require("minimatch");
119
const Context = require("../context.mjs").Context;
1210
const collectFiles = require("./collect-files");
13-
const runHelpers = require("./run-helpers");
1411
const { logSymbols } = require("../utils");
1512

1613
/**
@@ -57,7 +54,7 @@ exports.watchParallelRun = (
5754
return createWatcher(mocha, {
5855
watchFiles,
5956
watchIgnore,
60-
async beforeRun({ mocha }) {
57+
beforeRun({ mocha }) {
6158
// I don't know why we're cloning the root suite.
6259
const rootSuite = mocha.suite.clone();
6360

@@ -69,12 +66,9 @@ exports.watchParallelRun = (
6966
// test depends on this module.
7067
const Mocha = require("../mocha");
7168

72-
// reload rootHooks and other plugins, in case those files have changed
73-
const plugins = await runHelpers.handleRequires(mocha.options.require);
74-
7569
// ... and now that we've gotten a new module, we need to use it again due
7670
// to `mocha.ui()` call
77-
const newMocha = new Mocha(Object.assign({}, mocha.options, plugins));
71+
const newMocha = new Mocha(mocha.options);
7872
// don't know why this is needed
7973
newMocha.suite = rootSuite;
8074
// nor this
@@ -120,7 +114,9 @@ exports.watchRun = (mocha, { watchFiles, watchIgnore }, fileCollectParams) => {
120114
return createWatcher(mocha, {
121115
watchFiles,
122116
watchIgnore,
123-
async beforeRun({ mocha }) {
117+
beforeRun({ mocha }) {
118+
mocha.unloadFiles();
119+
124120
// I don't know why we're cloning the root suite.
125121
const rootSuite = mocha.suite.clone();
126122

@@ -132,17 +128,9 @@ exports.watchRun = (mocha, { watchFiles, watchIgnore }, fileCollectParams) => {
132128
// test depends on this module.
133129
const Mocha = require("../mocha");
134130

135-
// reload rootHooks and other plugins, in case those files have changed
136-
const plugins = await runHelpers.handleRequires(mocha.options.require, {
137-
// disable requireOrImport, since if the module gets loaded via ESM
138-
// the cache won't get busted between runs.
139-
// watch serial mode doesn't currently support ESM anyway.
140-
forceRequire: true,
141-
});
142-
143131
// ... and now that we've gotten a new module, we need to use it again due
144132
// to `mocha.ui()` call
145-
const newMocha = new Mocha({ ...mocha.options, ...plugins });
133+
const newMocha = new Mocha(mocha.options);
146134
// don't know why this is needed
147135
newMocha.suite = rootSuite;
148136
// nor this
@@ -397,19 +385,6 @@ function createPathMatcher(allowed, ignored, basePath) {
397385
return matcher;
398386
}
399387

400-
const TEST_MODE = Boolean(process.env.__MOCHA_WATCH_MOCK_CHOKIDAR);
401-
402-
// grab Date.now before anything like sinon.createFakeTimers can monkeypatch it
403-
// eslint-disable-next-line no-restricted-globals
404-
const dateNow = Date.now;
405-
if (TEST_MODE) debug("in test mode");
406-
407-
const sendMessage = (msg) => {
408-
if (!TEST_MODE) return;
409-
process.send(msg);
410-
debug("sent message", msg);
411-
};
412-
413388
/**
414389
* Bootstraps a Chokidar watcher. Handles keyboard input & signals
415390
* @param {Mocha} mocha - Mocha instance
@@ -454,70 +429,29 @@ const createWatcher = (
454429
const matcher = createPathMatcher(allowed, ignored, basePath);
455430

456431
// Chokidar has to watch the directory paths in case new files are created
457-
let watcher;
458-
/* istanbul ignore next */
459-
if (TEST_MODE) {
460-
const EventEmitter = require("node:events");
461-
watcher = new EventEmitter();
462-
process.on("message", (msg) => {
463-
debug("got message", msg);
464-
if (msg.watcher) watcher.emit(...msg.watcher);
465-
});
466-
} else {
467-
watcher = chokidar.watch(Array.from(allowed.dir.paths), {
468-
ignoreInitial: true,
469-
ignored: matcher.ignore,
470-
alwaysStat: true,
471-
});
472-
}
473-
474-
// if files change during global setup, we need to blast the cache before the first
475-
// run in case any tests depend on changed modules that were loaded by global setup
476-
let blastCacheBeforeNextRun = false;
477-
const rerunner = createRerunner(mocha, watcher, matcher, {
478-
beforeRun: async (opts) => {
479-
if (blastCacheBeforeNextRun) {
480-
blastCacheBeforeNextRun = false;
481-
blastCache(matcher);
482-
}
483-
return await beforeRun(opts);
484-
},
432+
const watcher = chokidar.watch(Array.from(allowed.dir.paths), {
433+
ignoreInitial: true,
434+
ignored: matcher.ignore,
485435
});
486436

487-
const startTime = dateNow();
488-
debug("start time: %d", startTime);
437+
const rerunner = createRerunner(mocha, watcher, {
438+
beforeRun,
439+
});
489440

490-
watcher.on("all", function handleEvent(...args) {
491-
const [event, filePath, stat] = args;
492-
/* istanbul ignore next */
493-
if (TEST_MODE) sendMessage({ received: ["all", ...args] });
494-
if (exiting) return;
495-
if (!matcher.allow(filePath)) {
496-
debug("event doesn't match filter: %s, %s", event, filePath);
497-
return;
498-
}
499-
if (
500-
event === "unlink" ||
501-
// birthtime can be > mtime on some filesystems; also check ctime just in case
502-
(stat && Math.max(stat.mtime, stat.ctime, stat.birthtime) > startTime)
503-
) {
504-
// we don't want to accidentally trigger a run before globalFixtureContext
505-
// has been created. If it hasn't then it's okay to do nothing here because
506-
// the code that creates globalFixtureContext will run the tests afterward
507-
if (globalFixtureContext) rerunner.scheduleRun();
508-
// if files change during global setup, we need to blast the cache before the first
509-
// run in case any tests depend on changed modules that were loaded by global setup
510-
else blastCacheBeforeNextRun = true;
441+
watcher.on("ready", async () => {
442+
debug("watcher ready");
443+
if (!globalFixtureContext) {
444+
debug("triggering global setup");
445+
globalFixtureContext = await mocha.runGlobalSetup();
511446
}
447+
rerunner.run();
512448
});
513449

514-
/* istanbul ignore next */
515-
if (TEST_MODE) sendMessage({ listening: true, startTime });
516-
517-
debug("triggering global setup");
518-
mocha.runGlobalSetup().then((context) => {
519-
globalFixtureContext = context;
520-
return rerunner.run();
450+
watcher.on("all", (_event, filePath) => {
451+
// only allow file paths that match the allowed patterns
452+
if (matcher.allow(filePath)) {
453+
rerunner.scheduleRun();
454+
}
521455
});
522456

523457
hideCursor();
@@ -575,71 +509,51 @@ const createWatcher = (
575509
*
576510
* @param {Mocha} mocha - Mocha instance
577511
* @param {FSWatcher} watcher - Chokidar `FSWatcher` instance
578-
* @param {PathMatcher} matcher - `PathMatcher` instance
579-
* @param {Object} opts - Options!
580-
* @param {BeforeWatchRun} opts.beforeRun - Function to call before `mocha.run()`. Must be provided.
512+
* @param {Object} [opts] - Options!
513+
* @param {BeforeWatchRun} [opts.beforeRun] - Function to call before `mocha.run()`
581514
* @returns {Rerunner}
582515
* @ignore
583516
* @private
584517
*/
585-
const createRerunner = (mocha, watcher, matcher, { beforeRun }) => {
586-
// Set to `Runner.abort` when mocha is running. Set to another abort function
587-
// during `beforeRun()`. Set to `null` otherwise.
588-
let abortRun = null;
518+
const createRerunner = (mocha, watcher, { beforeRun } = {}) => {
519+
// Set to a `Runner` when mocha is running. Set to `null` when mocha is not
520+
// running.
521+
let runner = null;
589522

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

593-
const run = async () => {
526+
const run = () => {
594527
try {
595-
if (TEST_MODE) sendMessage({ run: true });
596-
let aborted = false;
597-
abortRun = () => (aborted = true);
598-
mocha = (await beforeRun({ mocha, watcher })) || mocha;
599-
if (aborted) {
600-
abortRun = null;
601-
blastCache(matcher);
602-
rerun();
603-
}
604-
const runner = mocha.run(() => {
528+
mocha = beforeRun ? beforeRun({ mocha, watcher }) || mocha : mocha;
529+
runner = mocha.run(() => {
605530
debug("finished watch run");
606-
if (TEST_MODE) sendMessage({ runFinished: true });
607-
abortRun = null;
608-
blastCache(matcher);
531+
runner = null;
532+
blastCache(watcher);
609533
if (rerunScheduled) {
610534
rerun();
611535
} else {
612536
console.error(`${logSymbols.info} [mocha] waiting for changes...`);
613537
}
614538
});
615-
abortRun = () => runner.abort();
616539
} catch (err) {
617-
abortRun = null;
618540
console.error(err.stack);
619541
}
620542
};
621543

622544
const scheduleRun = () => {
623-
if (TEST_MODE) sendMessage({ scheduleRun: true });
624545
if (rerunScheduled) {
625546
return;
626547
}
627548

628549
rerunScheduled = true;
629-
if (abortRun) {
630-
abortRun();
550+
if (runner) {
551+
runner.abort();
631552
} else {
632-
debounceRerun();
553+
rerun();
633554
}
634555
};
635556

636-
let rerunTimeout;
637-
638-
const debounceRerun = () => {
639-
clearTimeout(rerunTimeout);
640-
rerunTimeout = setTimeout(rerun, 10);
641-
};
642-
643557
const rerun = () => {
644558
rerunScheduled = false;
645559
eraseLine();
@@ -652,6 +566,25 @@ const createRerunner = (mocha, watcher, matcher, { beforeRun }) => {
652566
};
653567
};
654568

569+
/**
570+
* Return the list of absolute paths watched by a Chokidar watcher.
571+
*
572+
* @param watcher - Instance of a Chokidar watcher
573+
* @return {string[]} - List of absolute paths
574+
* @ignore
575+
* @private
576+
*/
577+
const getWatchedFiles = (watcher) => {
578+
const watchedDirs = watcher.getWatched();
579+
return Object.keys(watchedDirs).reduce(
580+
(acc, dir) => [
581+
...acc,
582+
...watchedDirs[dir].map((file) => path.join(dir, file)),
583+
],
584+
[],
585+
);
586+
};
587+
655588
/**
656589
* Hide the cursor.
657590
* @ignore
@@ -680,17 +613,14 @@ const eraseLine = () => {
680613

681614
/**
682615
* Blast all of the watched files out of `require.cache`
683-
* @param {PathMatcher} matcher - only delete keys allowed by this matcher
616+
* @param {FSWatcher} watcher - Chokidar FSWatcher
684617
* @ignore
685618
* @private
686619
*/
687-
const blastCache = (matcher) => {
688-
let deletedCount = 0;
689-
for (const key in require.cache) {
690-
if (matcher.allow(key)) {
691-
deletedCount++;
692-
delete require.cache[key];
693-
}
694-
}
695-
debug("deleted %d file(s) from the require cache", deletedCount);
620+
const blastCache = (watcher) => {
621+
const files = getWatchedFiles(watcher);
622+
files.forEach((file) => {
623+
delete require.cache[file];
624+
});
625+
debug("deleted %d file(s) from the require cache", files.length);
696626
};

0 commit comments

Comments
 (0)