Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@jedwards1211
Copy link

@jedwards1211 jedwards1211 commented Jul 30, 2025

PR Checklist

Overview

Turn off ignoreInitial and distinguish between initial 'add' events and paths added after test startup. We can compare the mtime of the path to the test startup time to decide if the change should trigger a rerun.

Also, this PR changes watch mode to reload --required files between runs.

@jedwards1211
Copy link
Author

jedwards1211 commented Jul 30, 2025

@mark-wiemer is using new Date() allowed in the CLI code? An ESLint rule complains about it. If not is there any way I can get the current time to compare to file timestamps?

Also, let me know if you have any opinions about if/how I should test this change. It would be nice if I could inject some kind of chokidar mock so I can control timing of the file events to be able to verify in a test that it runs before the ready event is emitted...but the only easy way I think of to do that would be to call things in watch-run.js directly from the test process instead of executing mocha --watch in a separate process.

@mark-wiemer mark-wiemer self-requested a review July 30, 2025 21:31
});

(async () => {
if (!globalFixtureContext) {
Copy link
Member

Choose a reason for hiding this comment

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

The earlier versions of Mocha using Chokidar 3 still waited until watcher.on('ready') to call mocha.runGlobalSetup(), but I'm not sure why. A quick look at the code isn't too revealing, but I see why we'd want to call this right away. I'm curious if we can add a test case that has a complex global setup to ensure things still work?

Copy link
Author

Choose a reason for hiding this comment

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

I'm not exactly familiar with what that global setup is (loading required modules?) but I can try to figure out what goes into it

Copy link
Author

@jedwards1211 jedwards1211 Aug 1, 2025

Choose a reason for hiding this comment

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

Okay Chokidar doesn't seem to specify whether 'change'/'unlink' events can get emitted before 'ready' if changes occur during its initial scan, but if so, it could cause the preexisting code here to start running tests before it's performed global setup.

Obviously the intention of the preexisting code is to wait for the 'ready' event to run everything for the first time (first global setup, then the first test run).

I've fixed this in my PR now, if it gets an event before the global fixture context has been created it won't schedule a rerun.

(I still need to write tests for this)

Copy link
Member

Choose a reason for hiding this comment

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

@jedwards1211 do we still need tests for this?

@mark-wiemer
Copy link
Member

Marking as draft until tests are added

@mark-wiemer mark-wiemer reopened this Aug 10, 2025
@mark-wiemer mark-wiemer marked this pull request as draft August 10, 2025 02:09
@jedwards1211
Copy link
Author

@mark-wiemer I want to run some changes by you before I dive into them...

I realized that GlobFilesTracker synchronously globs for the watched/ignored files, which mostly defeats the purpose of starting tests before the watcher is ready, since all that globbing would take about as long. So I think we should make it glob asynchronously.

Also...

  • It globs each watch pattern and each ignore pattern separately...but it seems like we can just pass all of the watch and ignore patterns to one glob call?
  • The existing code re-globs everything when the watcher gets a path added event, which could be a lot of redundant work...instead I think we should check if the added path is already in the watched or ignored set, and if not and it's a directory, initiate a glob on just that directory?

@mark-wiemer
Copy link
Member

Hey @jedwards1211, the current glob code is not ideal for sure for exactly the reasons you mentioned and probably more. The best discussion around this is #5379 and in short we don't want to touch this code until v12. We as maintainers don't have a ton of experience with glob matching and don't want to break anything, but we do want to keep Chokidar at v4. Mocha 11.2.1 used Chokidar v3, we upgraded to v4 in Mocha 11.2.2, but Chokidar v4 dropped glob support so we had to use our own. Learning more about globs is definitely a priority, but I'm still getting the ropes on the rest of Mocha as a new maintainer, so I won't have a ton of updates here.

In short, feel free to update or change or even rewrite the glob handling. Just keep #5379 in mind as another alternative and add as many tests as you can to help me and other maintainers verify any changes :)

Related issues include #5355 and #5374

@jedwards1211
Copy link
Author

jedwards1211 commented Aug 16, 2025

Yeah I kind of thought about doing the same thing as that PR.

One question is how to change blastCache - if we start and possibly even rerun before the watcher is finished scanning, we can't rely on getting the watched paths from chokidar. I already had to fix this in this PR, by getting the paths to delete from the GlobFilesTracker.

Instead we would have to delete all require.cache paths matching the filter. I worry a bit about how well that would perform compared to just deleting the list of matched paths we know about though. If there's a long list of watch patterns it could be a lot of work determining matches.

@mark-wiemer
Copy link
Member

I wouldn't worry about performance too much in the abstract. We had a concrete issue with #5374 that has effectively been resolved with #5379, and in practice I can't imagine folks are using many regexes for watch paths. If we get a bug report about perf, we can look into it, but as long as our basic stress tests pass, I'd prefer simpler changes first.

On another note, @mochajs/maintenance-crew I'm thinking of marking this as "semver-major" given the complexity impacted and possibility of bugs. Since we want Mocha 12 to have minimal changes we'll probably hold this one for Mocha 13. (#5357)

@jedwards1211
Copy link
Author

jedwards1211 commented Aug 16, 2025

Okay I guess I'll eventually piggyback this on #5379 and blast the cache with the new filtering from that PR.

Also, I'm not sure what to do about getting the launch time to compare file mtimes to. Date etc could get monkeypatched by test utils like Sinin fake timers... do you have any thoughts?

I guess it won't get monkeypatched before we run global setup though. But I got an ESLint error about using Date, it must be for good reason.

@mark-wiemer
Copy link
Member

I'm not sure why our linter warns about using Date. Could you see if that's a recommended rule? There's a real chance that our linter is very out of date. We've had some discussions about it but haven't prioritized updates there for a while.

@jedwards1211
Copy link
Author

jedwards1211 commented Aug 17, 2025

@mark-wiemer looks like the rule was explicitly added to guard against exactly the danger of fake timers I was talking about: #237

(This issue is explicitly linked in your ESLint config)

Note, it would probably be possible to grab a reference to the original Date constructor, Date.now() before it can get monkeypatched, and use that if necessary throughout Mocha.

As I was saying though, in this case I only need to get the launch date before running any global setup where userland code might monkeypatch Date, so I guess it's okay to suppress the error in this case

@mark-wiemer
Copy link
Member

Thanks for researching. The core concern still applies, but I think the workaround does too: just have Date = global.Date at the top-level and then call Date() within your function as needed. Sinon mocks global.Date once the tests start running, but if we do this at top-level, it's done before the tests are run. As long as we never call global.Date() within a function we are good. I think... once the code is ready I'll definitely ping other maintainers to review it!

@JoshuaKGoldberg JoshuaKGoldberg added the status: waiting for author waiting on response from OP or other posters - more information needed label Sep 30, 2025
@JoshuaKGoldberg
Copy link
Member

👋 Just checking in @jedwards1211, are you waiting on us for anything?

@jedwards1211
Copy link
Author

No, I'll pick this back up soon now that that related PR is merged

@jedwards1211 jedwards1211 marked this pull request as ready for review October 9, 2025 16:06
@jedwards1211
Copy link
Author

jedwards1211 commented Oct 9, 2025

@mark-wiemer okay, to test timing-sensitive cases, I used an IPC channel to send mock chokidar events and tell the mocha process when to pretend global setup is done. I turned this mode on by passing a MOCHA_TEST_WATCH environment variable, but I'm not sure if you'd want this...if not how do you want me to accomplish the mocking?

@mark-wiemer mark-wiemer removed the status: waiting for author waiting on response from OP or other posters - more information needed label Oct 12, 2025
@mark-wiemer mark-wiemer self-requested a review October 12, 2025 18:22
@mark-wiemer mark-wiemer modified the milestones: v12.0.0, v12.x Jan 3, 2026
@mark-wiemer
Copy link
Member

Hi @jedwards1211, moved this to 12.x instead of 12.0, would still love to merge it but just clarifying that it isn't necessary for us to release Mocha 12. Are you able to continue working on this?

@jedwards1211
Copy link
Author

Yeah, if there's any way I could have permission to retrigger CI it would help me vet it done sooner

@JoshuaKGoldberg
Copy link
Member

One strategy I've done for situations like this is to fork the upstream repo and send a PR within that repo to itself. That way you don't have to wait on upstream things.

@jedwards1211
Copy link
Author

Oh I forgot I could do that, thanks for the tip!

@mark-wiemer
Copy link
Member

@jedwards1211 we also have our test workflow set to workflow_dispatch which allows you to manually trigger runs on any branch. For you that only applies to your fork, of course, but it's nice if you want to detect a flaky issue :) feel free to reach out in the Discord for detailed guidance!

@mark-wiemer
Copy link
Member

@jedwards1211 congrats on getting the tests passing!! I'll check this one out soon. @JoshuaKGoldberg I think we can put this in 12.0, what do you think?

@mark-wiemer mark-wiemer added status: needs review a maintainer should (re-)review this pull request and removed status: waiting for author waiting on response from OP or other posters - more information needed labels Jan 4, 2026
@jedwards1211
Copy link
Author

jedwards1211 commented Jan 4, 2026

In CI within my fork I noticed a preexisting test flaking out once, only getting one of two expected test runs in the output. I think this is the brittleness of sleep timing based approaches. If you want I could rewrite more of the existing watch tests to wait for messages from the mocha process instead of sleeping to mitigate the flakiness

(A few of my tests mock chokidar to control timing of even its ready event, but I would leave existing tests using real chokidar. I would just tell the mocha process to send messages when it receives chokidar events, finishes a test run, etc and make tests wait for those events instead of just sleeping.)

@jedwards1211
Copy link
Author

jedwards1211 commented Jan 4, 2026

Btw, the final change that fixed the tests was checking if any of the mtime, ctime or birthtime of a file is after the test start time. Before I was only checking the mtime, but that can actually be before the birthtime on Windows (fs.cpSync may preserve mtime but set a new birthtime).

@mark-wiemer
Copy link
Member

I could rewrite more of the existing watch tests

You're welcome to open a second PR for this if you'd like, but let's keep this PR to its current scope :)

@mark-wiemer
Copy link
Member

Just FYI, might be a few days before I have enough energy to dedicate time to reviewing this, it's a very sensitive set of changes

@jedwards1211
Copy link
Author

Okay sounds good

debug("resolved required file %s to %s", mod, modpath);
}
const requiredModule = await requireOrImport(modpath);
const requiredModule = forceRequire
Copy link
Member

Choose a reason for hiding this comment

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

Changing handleRequires feels like it can be split into its own small PR with tests, am I correct there? Curious to track details about what problem this solves (e.g. create an issue for this showing why the change is needed)

Copy link
Member

Choose a reason for hiding this comment

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

haha with the scale of this PR, breaking out these few lines won't change much. a brief comment to cover why forceRequire is necessary would be appreciated here :)

*/
const createTempDir = async () => {
const dirpath = await fsP.mkdtemp(path.join(os.tmpdir(), "mocha-"));
const dirpath = await fsP.realpath(
Copy link
Member

Choose a reason for hiding this comment

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

Can you elaborate why realpath is needed here, maybe with a code comment?

/**
* Waits for an `EventEmitter` to emit the given `event`.
* @param {object} [opts]
* @param {EventFilter} [opts.filter] - predicate that will filter which event payloads will resolve the Promise
Copy link
Member

Choose a reason for hiding this comment

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

Can never be too clear!

Suggested change
* @param {EventFilter} [opts.filter] - predicate that will filter which event payloads will resolve the Promise
* @param {EventFilter} [opts.filter] - predicate that will filter which event payloads will resolve the Promise (defaults to `() => true`)

}

/**
* Waits for an `EventEmitter` to emit the given `event`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Waits for an `EventEmitter` to emit the given `event`.
* Returns a promise that resolves when the `emitter`
* emits the `event` with properties matching the `filter`

/**
* Predicate for `gotEvent` that will filter which event payloads will resolve the Promise
* @callback EventFilter
* @param {...?} payload - the payload emitted for the event
Copy link
Member

Choose a reason for hiding this comment

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

I think this function only ever takes one arg, right?

Suggested change
* @param {...?} payload - the payload emitted for the event
* @param {*} payload - the payload emitted for the event

"ignore": [
"test/integration/fixtures/esm/type-module/test-that-imports-non-existing-module.fixture.js",
"test/integration/fixtures/options/watch/test-with-dependency.fixture.js",
"test/integration/fixtures/options/watch/test-with-dependency-and-delay.fixture.js",
Copy link
Member

@mark-wiemer mark-wiemer Jan 8, 2026

Choose a reason for hiding this comment

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

Suggested change
"test/integration/fixtures/options/watch/test-with-dependency-and-delay.fixture.js",
// import is only resolvable while running, not on a clean checkout
"test/integration/fixtures/options/watch/test-with-dependency-and-delay.fixture.js",

"test-node:only:globalBdd": "npm run -s test-node-run-only -- --ui bdd test/only/global/bdd.spec --no-parallel",
"test-node:only:globalQunit": "npm run -s test-node-run-only -- --ui qunit test/only/global/qunit.spec --no-parallel",
"test-node:only:globalTdd": "npm run -s test-node-run-only -- --ui tdd test/only/global/tdd.spec --no-parallel",
"test-node:only:integration:watch": "npm run -s test-node-run-only -- --no-parallel --timeout 10000 --slow 3750 \"test/integration/options/watch.spec.js\"",
Copy link
Member

Choose a reason for hiding this comment

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

can you clarify why this new command is needed?

@@ -0,0 +1,8 @@
const dependency = require('./lib/dependency');

it('checks dependency', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
it('checks dependency', async () => {
// sleep and then check for existing depencency.fixture.js
it('checks dependency', async () => {

);
}

it("works when watcher is ready before global setup finishes", async function () {
Copy link
Member

Choose a reason for hiding this comment

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

Let's clean up these tests. I am thinking something like "before watcher is ready" becomes its own suite.

e.g. handles files added before watcher is ready with timestamp after start

becomes before watcher is ready > files added > handles timestamp after start

that should help us de-dupe boilerplate here as well.

Let me know if you have questions. Goal is to keep test names short, even if they're deeply nested. Then we can better review if all scenarios are covered.

// ... and now that we've gotten a new module, we need to use it again due
// to `mocha.ui()` call
const newMocha = new Mocha(mocha.options);
const newMocha = new Mocha(Object.assign({}, mocha.options, plugins));
Copy link
Member

Choose a reason for hiding this comment

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

Should be the same, just rewritten in a way I find a bit more readable

Suggested change
const newMocha = new Mocha(Object.assign({}, mocha.options, plugins));
const newMocha = new Mocha({
...mocha.options,
plugins
});

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, it looks like this second suggestion is actually the same behavior, but I'm not sure why we're spreading ...plugins

Suggested change
const newMocha = new Mocha(Object.assign({}, mocha.options, plugins));
const newMocha = new Mocha({ ...mocha.options, ...plugins });

Copy link
Member

@mark-wiemer mark-wiemer Jan 8, 2026

Choose a reason for hiding this comment

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

let's make sure we have a unit test that covers when plugins are present vs not present

Copy link
Member

@mark-wiemer mark-wiemer left a comment

Choose a reason for hiding this comment

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

Very close, I promise! If we clean up tests I'll have much more confidence in this :)

@mark-wiemer mark-wiemer added status: waiting for author waiting on response from OP or other posters - more information needed and removed status: needs review a maintainer should (re-)review this pull request labels Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting for author waiting on response from OP or other posters - more information needed

Projects

Status: No status

3 participants