Skip to content

Commit d38be99

Browse files
test: poll at the gap the watch suites write at (#22181)
* test: poll at the gap the watch suites write at `runtimes (deno)` fails intermittently on WatchDetection's tightest case — "time between changes 10ms with invalidate call" — waiting out jest's 30s budget for `done()`. Three things around it were wrong. The interval was an order of magnitude coarser than the gap it had to resolve: the harness forces `WATCHPACK_POLLING=100` because Deno's and Bun's `node:fs.watch` drop change events under jest's worker threads, while that case writes the same file twice 10ms apart. Both setups now poll at 10ms, which is no slower — measured over a 40-case slice of WatchTestCases and of WatchCacheUnaffectedTestCases, 16.9s against 18.4s at 100ms. The test's own `poll: 100` never applied: `DirectoryWatcher` assigns `options.poll = FORCE_POLLING` on every watcher whenever the env var is set, so the option could not differ from it and tuning it did nothing. It is gone, and the comment says where the interval comes from. And jest's default budget is not this test's to keep: where the harness polls, each change costs an interval to notice and this case walks several change-and-rebuild round trips, so it now gets 120s there and the default everywhere else. * test: retry the change WatchDetection's last step waits on The first attempt was wrong. Polling at the gap the case writes at did not fix the `runtimes (deno)` failure — it failed again on that head, and at 120000ms, so the test stalls rather than runs slow and no budget rescues it. Both the interval and the raised timeout are reverted; the timeout only made a red run 90s longer. The stall is structural. Every other step advances on its own timer, but the last one advances only when a `done` arrives carrying "correct", and nothing retries. A watcher that drops or coalesces that single change strands the test forever — which is what Deno's node:fs.watch compat does under jest's worker threads, and why polling is forced there at all. It now re-touches the file until the rebuild carrying it arrives, so a dropped event costs a retry instead of the suite. The assertion is unchanged: webpack still has to rebuild and put "correct" in the bundle. Also drops the test's own `poll` option, which never applied: DirectoryWatcher assigns options.poll = FORCE_POLLING on every watcher whenever WATCHPACK_POLLING is set, so it could not differ from it. * test: clear the watch retry interval on every exit path
1 parent 1d75746 commit d38be99

1 file changed

Lines changed: 19 additions & 9 deletions

File tree

‎test/unitCases/WatchDetection.unittest.js‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ describe("WatchDetection", () => {
4444
const filePath = path.join(fixturePath, "file.js");
4545
const file2Path = path.join(fixturePath, "file2.js");
4646
const loaderPath = path.join(testDirectory, "fixtures", "delay-loader.js");
47+
// Scoped here rather than to the test: a timed-out or errored test never
48+
// reaches its own cleanup, and an interval still writing into the fixture
49+
// during `afterAll` fails the case after it has already finished.
50+
/** @type {NodeJS.Timeout | undefined} */
51+
let retry;
4752

4853
beforeAll(() => {
4954
try {
@@ -56,6 +61,7 @@ describe("WatchDetection", () => {
5661
});
5762

5863
afterAll((done) => {
64+
if (retry) clearInterval(retry);
5965
setTimeout(() => {
6066
try {
6167
fs.unlinkSync(filePath);
@@ -116,16 +122,11 @@ describe("WatchDetection", () => {
116122
}
117123
};
118124

125+
// No `poll` here: watchpack's WATCHPACK_POLLING overrides the option
126+
// on every watcher, and the harness sets it for the runtimes whose
127+
// native watch is unreliable.
119128
watcher = /** @type {import("../../").Watching} */ (
120-
compiler.watch(
121-
{
122-
aggregateTimeout: 50,
123-
// Deno's node:fs.watch compat drops/delays change events, so
124-
// native detection is flaky here; poll for deterministic pickup.
125-
...(process.versions.deno ? { poll: 100 } : {})
126-
},
127-
() => {}
128-
)
129+
compiler.watch({ aggregateTimeout: 50 }, () => {})
129130
);
130131
}
131132

@@ -175,14 +176,23 @@ describe("WatchDetection", () => {
175176
}
176177
};
177178

179+
// This is the one step that advances only on a `done` carrying the new
180+
// content, so a watcher that drops or coalesces this single change
181+
// strands it. Re-touch until the rebuild carrying it arrives.
178182
fs.writeFile(file2Path, "correct", "utf8", handleError);
183+
retry = setInterval(() => {
184+
fs.writeFile(file2Path, "correct", "utf8", handleError);
185+
}, 500);
186+
retry.unref();
179187
}
180188

181189
/**
182190
* @returns {void}
183191
*/
184192
function step5() {
185193
onChange = null;
194+
if (retry) clearInterval(retry);
195+
retry = undefined;
186196

187197
watcher.close(() => {
188198
setTimeout(done, 500);

0 commit comments

Comments
 (0)