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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Kartones
Copy link

@Kartones Kartones commented Jun 3, 2024

PR Checklist

Overview

After filling #5149, I had some time to try to narrow the problem.

When running mocha in non-parallel watch mode, we are not spawning new processes, so the initial require hooks get loaded once, but on reloads/reruns, if they contained some stateful dependency, each subsequent run gets a new set of mocha requires for the test files, but not those global hooks, so they keep their initial state, and effectively the tests now run with a different context and can't affect/read the global hooks.
This issue can only be avoided by watching also the root hooks and their dependencies, which in large projects is not always easy to do (and can lead to big watched path lists in large repositories).

Explained with code:

  • watchRun.createWatcher -> beforeRun does const newMocha = new Mocha(mocha.options); which means that the global hooks (stored inside mocha.options keep their initial load's closure.
  • watchRun.createRerunner -> run performs a blastCache(), but that blasting is only aware of watched files.
  • When re-running, blastCache() causes the tests to re-import/re-require, creating a new closure for their stateful dependency; If all files are watched, that's fine, but just by not watching the global hooks (I added a test for this scenario), beforeRun creates the newMocha but mocha.options are the same, effectively now having two scopes, one with the original global hooks and another for the test run.

My changes fix this scenarios by:

  • Extracting handleRequires() to a separate file (else there would be a circular dependency between run-helpers and watch-run)
  • Adding a parameter to blastCache to do a full cache blast
    • when not running in parallel, as parallel mode is not affected
    • full blast empties the require.cache, but for import there's no alternative than to play around with the querystring, so it changes its key (default is "", so only changes for non-parallel watch reruns)
  • Modifying watchRun.createWatcher -> beforeRun so that it reloads the global hooks (via handleRequires()), which means that that each rerun will have the same new mocha.options closure as the tests it'll run.

I've added two test scenarios:

  • simple one: a test with an unwatched stateful dependency. The test mutates the dep's state in a way that reruns will fail, unless we fully blast & reimport.
  • complex one: a test with unwatched dependency & unwatched hook. The hook mutates the dependency state in a beforeHook, and the test has before and afterEach hooks to mutate it again to attempt to make it fail, so will only pass if the global hook is reloaded and the test picks the same closure.

As mentioned, I wanted to avoid full blasts when not needed, so parallel watch mode is not affected. An alternative is to always do a full blast, and then we can remove some conditional logic and simplify a bit.

Let me know if I should further explain something.

Copy link

linux-foundation-easycla bot commented Jun 3, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@Kartones Kartones force-pushed the clear-require-cache-in-watch-mode branch from 1b588e8 to 55d042e Compare June 3, 2024 14:24
@Kartones
Copy link
Author

Kartones commented Jun 4, 2024

Added 24aaa001911cd78da5dc38898f2443121491cfbf because testing the changes on a real big project, I got the following:

[mocha] waiting for changes...

  ATest
Error: Mocha instance is already disposed, it cannot be used again.

@Kartones Kartones marked this pull request as draft June 4, 2024 09:47
@Kartones

This comment was marked as resolved.

@Kartones
Copy link
Author

Kartones commented Jun 4, 2024

7f26597 Does not touch ESM imports if the cache busting key is empty, and the busting logic takes into account URL objects, not only strings.

@Kartones Kartones marked this pull request as ready for review June 4, 2024 11:18
@coveralls
Copy link

Coverage Status

coverage: 94.399% (+0.04%) from 94.358%
when pulling 7f26597 on Kartones:clear-require-cache-in-watch-mode
into 472a8be on mochajs:master.

Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

👏 What a fantastic investigation, thanks for sending this in @Kartones (and sorry for taking so long to get back to you)!

I'm in favor of this general change - I think it's a bug to not aggressively clear the cache between reruns. But (also posting in the issue):

  • This is a major version breaking change and so won't land for a little while
  • I'd want to also hear from @mochajs/maintenance-crew on what else we want to do

Requesting changes on the one [Bug].

Cheers!

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/..).

@@ -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.

[Praise] The test coverage in this PR is fantastic, nicely done!

delete require.cache[file];
});
debug('deleted %d file(s) from the require cache', files.length);
}
Copy link
Member

Choose a reason for hiding this comment

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

[Style] Totally optional, but we could deduplicate the internals of this function a bit:

  const files = blastAll
    ? Object.keys(require.cache)
      // Avoid deleting mocha binary (at minimum, breaks Mocha's watch tests)
      .filter(file => !file.includes('/mocha/bin/'))
    : getWatchedFiles(watcher);

  files.forEach(file => {
    delete require.cache[file];
  });

  debug('deleted %d file(s) from the require cache (blastAll: %s)', files.length, blastAll);

No worries if you hate this 🙂

Copy link
Author

Choose a reason for hiding this comment

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

If we always clean the whole cache, then there's no need at all for the conditional logic, so I removed it. What I did like and left was the count of deleted files for the debug statement, might be useful.

@JoshuaKGoldberg JoshuaKGoldberg added the semver-major implementation requires increase of "major" version number; "breaking changes" label Jul 3, 2024
@Kartones
Copy link
Author

I bring sad news.

I tried the changes at work, where we have a complex setup, with many tests and a few hooks. With Mocha 10.4 it fails upon reload in watch mode (expected). When I tried with this PR's branch, it got in a loop of erroring and attempting to rerun the tests. I added some traces (and a base36 uniqueId per mocha instance), and ran the watch mode again.

I think that the issue is that, as this PR places handleRequires (which is async) on a previously synchronous code flow, there is a place where we are not correctly awaiting. It could be the chokidar watcher, but in theory I should also make mocha runner.run() async... And that's a road that I don't think it sensible to walk right now.

I will see if there is any alternative, but from a quick look, it does not look not easy, as nodejs/esm-utils.js uses import() and that is forcibly asynchronous.

Here is the full trace (with tiny redactions, mostly on paths), in case it sheds some light, or anyone has some idea:

n8cpvox6aaf watchRun.createWatcher()
n8cpvox6aaf watchRun.createRerunner()
n8cpvox6aaf watchRun.createWatcher.on-all()
n8cpvox6aaf watchRun.createRerunner.scheduleRun()
n8cpvox6aaf watchRun.createRerunner.rerun()
n8cpvox6aaf watchRun.createRerunner.run()
n8cpvox6aaf watchRun.createWatcher.beforeRun()
n8cpvox6aaf STATE WHEN UNLOADING: init
n8cpvox6aaf STATE AFTER UNLOADING: init
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.on-all()
n8cpvox6aaf watchRun.createRerunner.scheduleRun()
n8cpvox6aaf watchRun.createRerunner.rerun()
n8cpvox6aaf watchRun.createRerunner.run()
n8cpvox6aaf watchRun.createWatcher.beforeRun()
n8cpvox6aaf STATE WHEN UNLOADING: init
n8cpvox6aaf STATE AFTER UNLOADING: init
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> after handleRequires()
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> before cloning mocha suite
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> after cloning mocha suite
n8cpvox6aaf STATE WHEN DISPOSING: init
n8cpvox6aaf STATE WHEN UNLOADING: init
n8cpvox6aaf STATE AFTER UNLOADING: init
n8cpvox6aaf STATE AFTER DISPOSING: disposed
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> after mocha.dispose()
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> after handleRequires()
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> before cloning mocha suite
n8cpvox6aaf watchRun.createWatcher.beforeRun() -> after cloning mocha suite
n8cpvox6aaf STATE WHEN DISPOSING: disposed
n8cpvox6aaf STATE WHEN UNLOADING: disposed
ATest tests
Error: Mocha instance is already disposed, it cannot be used again.
at createMochaInstanceAlreadyDisposedError (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/errors.js:377:13)
at Mocha.unloadFiles (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:498:11)
at Mocha.dispose (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:638:8)
at beforeRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:124:13)
at run (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:290:28)
at rerun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:330:5)
at Object.scheduleRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:321:7)
at FSWatcher.<anonymous> (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:211:5)

ATest
✔ test 1
✔ test 2
✔ test 3
✔ test 4
✔ test 5
✔ test 6
n8cpvox6aaf watchRun.createWatcher.on-all()
olgtbdzkej8 watchRun.createRerunner.scheduleRun()
✔ test 7

7 passing (92ms)

olgtbdzkej8 watchRun.createRerunner.run() -> mocha.run()
olgtbdzkej8 scheduled a rerun
olgtbdzkej8 watchRun.createRerunner.rerun()
olgtbdzkej8 watchRun.createRerunner.run()
olgtbdzkej8 watchRun.createWatcher.beforeRun()
olgtbdzkej8 STATE WHEN UNLOADING: referencesCleaned
olgtbdzkej8 STATE AFTER UNLOADING: init
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.on-all()
olgtbdzkej8 watchRun.createRerunner.scheduleRun()
olgtbdzkej8 watchRun.createRerunner.rerun()
olgtbdzkej8 watchRun.createRerunner.run()
olgtbdzkej8 watchRun.createWatcher.beforeRun()
olgtbdzkej8 STATE WHEN UNLOADING: init
olgtbdzkej8 STATE AFTER UNLOADING: init
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.on-all()
olgtbdzkej8 watchRun.createRerunner.scheduleRun()
olgtbdzkej8 watchRun.createRerunner.rerun()
olgtbdzkej8 watchRun.createRerunner.run()
olgtbdzkej8 watchRun.createWatcher.beforeRun()
olgtbdzkej8 STATE WHEN UNLOADING: init
olgtbdzkej8 STATE AFTER UNLOADING: init
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before handleRequires()
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after handleRequires()
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before cloning mocha suite
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after cloning mocha suite
olgtbdzkej8 STATE WHEN DISPOSING: init
olgtbdzkej8 STATE WHEN UNLOADING: init
olgtbdzkej8 STATE AFTER UNLOADING: init
olgtbdzkej8 STATE AFTER DISPOSING: disposed
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after mocha.dispose()
ATest tests

ATest
✔ test 1
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after handleRequires()
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before cloning mocha suite
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after cloning mocha suite
olgtbdzkej8 STATE WHEN DISPOSING: disposed
olgtbdzkej8 STATE WHEN UNLOADING: disposed
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after handleRequires()
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> before cloning mocha suite
olgtbdzkej8 watchRun.createWatcher.beforeRun() -> after cloning mocha suite
olgtbdzkej8 STATE WHEN DISPOSING: disposed
olgtbdzkej8 STATE WHEN UNLOADING: disposed
Error: Mocha instance is already disposed, it cannot be used again.
at createMochaInstanceAlreadyDisposedError (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/errors.js:377:13)
at Mocha.unloadFiles (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:498:11)
at Mocha.dispose (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:638:8)
at beforeRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:124:13)
at run (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:290:28)
at rerun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:330:5)
at Object.scheduleRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:321:7)
at FSWatcher.<anonymous> (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:211:5)
Error: Mocha instance is already disposed, it cannot be used again.
at createMochaInstanceAlreadyDisposedError (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/errors.js:377:13)
at Mocha.unloadFiles (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:498:11)
at Mocha.dispose (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/mocha.js:638:8)
at beforeRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:124:13)
at run (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:290:28)
at rerun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:330:5)
at Object.scheduleRun (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:321:7)
at FSWatcher.<anonymous> (/a/path/node_modules/.pnpm/pr-path/node_modules/mocha/lib/cli/watch-run.js:211:5)
n8cpvox6aaf watchRun.createWatcher.on-all()
euwshxb0cl5 watchRun.createRerunner.scheduleRun()

1 passing (36ms)

euwshxb0cl5 watchRun.createRerunner.run() -> mocha.run()
euwshxb0cl5 scheduled a rerun
euwshxb0cl5 watchRun.createRerunner.rerun()
euwshxb0cl5 watchRun.createRerunner.run()
euwshxb0cl5 watchRun.createWatcher.beforeRun()
euwshxb0cl5 STATE WHEN UNLOADING: referencesCleaned
euwshxb0cl5 STATE AFTER UNLOADING: init
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.on-all()
euwshxb0cl5 watchRun.createRerunner.scheduleRun()
euwshxb0cl5 watchRun.createRerunner.rerun()
euwshxb0cl5 watchRun.createRerunner.run()
euwshxb0cl5 watchRun.createWatcher.beforeRun()
euwshxb0cl5 STATE WHEN UNLOADING: init
euwshxb0cl5 STATE AFTER UNLOADING: init
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> before handleRequires()
n8cpvox6aaf watchRun.createWatcher.on-all()
euwshxb0cl5 watchRun.createRerunner.scheduleRun()
euwshxb0cl5 watchRun.createRerunner.rerun()
euwshxb0cl5 watchRun.createRerunner.run()
euwshxb0cl5 watchRun.createWatcher.beforeRun()
euwshxb0cl5 STATE WHEN UNLOADING: init
euwshxb0cl5 STATE AFTER UNLOADING: init
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> before handleRequires()
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> after handleRequires()
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> before cloning mocha suite
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> after cloning mocha suite
euwshxb0cl5 STATE WHEN DISPOSING: init
euwshxb0cl5 STATE WHEN UNLOADING: init
euwshxb0cl5 STATE AFTER UNLOADING: init
euwshxb0cl5 STATE AFTER DISPOSING: disposed
euwshxb0cl5 watchRun.createWatcher.beforeRun() -> after mocha.dispose()

@Kartones
Copy link
Author

In the meantime, the quick solution to this cache issue, for those facing it, is to run in parallel-watch mode (--parallel --watch). It is a bit slower, but works fine.

// Avoid deleting mocha binary
.filter(
file =>
!file.includes('mocha/bin/mocha.js') &&
Copy link
Author

Choose a reason for hiding this comment

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

Also doing some tests, to avoid extra invalidations, this should be node_modules/mocha/. Confirmed that that single filtering works with Mocha tests, standard mocha dependency/package, and a custom repository & branch path (at least under PNPM).

@JoshuaKGoldberg JoshuaKGoldberg changed the title Fully clear require/import cache in non-parallel watch mode chore: fully clear require/import cache in non-parallel watch mode Sep 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-major implementation requires increase of "major" version number; "breaking changes"
Projects
None yet
Development

Successfully merging this pull request may close these issues.

🐛 Bug: Watch + mochaHooks inconsistent state on re-runs
3 participants