Files
node/test/sequential/test-watch-mode-restart-esm-loading-error.mjs
Christian Aurich bfaefce3a8 test: deflake test-watch-mode-restart-esm-loading-error
The previous deflake of this test, #63390, gave the two restart
waits common.platformTimeout(10_000) and left the first restart()
on the helper's 1000 ms default. That first call is the one still
timing out on CI.

It is also the call that needs the budget most. --watch spawns a
child process to run the script, so reaching the first Completed
running costs two process startups, while the later restarts pay for
one. Wrapping the default in platformTimeout() would not have been
enough on its own: none of its multipliers reaches a factor of ten,
so the budget would still be 1000 ms on these jobs, or 2000 ms in a
debug build.

Give that call the budget the other two already ask for, stated once in
the default rather than three times. The timer bounds how long the test
waits for watch mode to make progress, it is not an assertion about
restart latency, and the assertions on stdout and stderr are untouched.

The same helper is duplicated in test-watch-mode.mjs, with the same
default and nine call sites that take it. That file has carried
PASS, FLAKY since #44898, which is why only this one surfaces in the
reliability reports.

Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65623
Refs: https://github.com/nodejs/node/pull/63390
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-18.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-21.md
Refs: https://github.com/nodejs/node/issues/44898
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2026-08-31 17:24:54 +00:00

144 lines
4.4 KiB
JavaScript

import * as common from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import assert from 'node:assert';
import path from 'node:path';
import { execPath } from 'node:process';
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { inspect } from 'node:util';
import { createInterface } from 'node:readline';
import { setTimeout as sleep } from 'node:timers/promises';
if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
if (common.isAIX)
common.skip('AIX does not reliably capture syntax errors in watch mode');
let tmpFiles = 0;
function createTmpFile(content = 'console.log("running");', ext = '.js', basename = tmpdir.path) {
const file = path.join(basename, `${tmpFiles++}${ext}`);
writeFileSync(file, content);
return file;
}
function runInBackground({ args = [], options = {}, completed = 'Completed running', shouldFail = false }) {
let future = Promise.withResolvers();
let child;
let stderr = '';
let stdout = [];
const run = () => {
args.unshift('--no-warnings');
child = spawn(execPath, args, { encoding: 'utf8', stdio: 'pipe', ...options });
child.stderr.on('data', (data) => {
stderr += data;
});
const rl = createInterface({ input: child.stdout });
rl.on('line', (data) => {
if (!data.startsWith('Waiting for graceful termination') && !data.startsWith('Gracefully restarted')) {
stdout.push(data);
if (data.startsWith(completed)) {
future.resolve({ stderr, stdout });
future = Promise.withResolvers();
stdout = [];
stderr = '';
} else if (data.startsWith('Failed running')) {
const settle = () => {
if (shouldFail) {
future.resolve({ stderr, stdout });
} else {
future.reject({ stderr, stdout });
}
future = Promise.withResolvers();
stdout = [];
stderr = '';
};
// If stderr is empty, wait for it to receive data before settling.
// This handles the race condition where stdout arrives before stderr.
if (stderr === '') {
child.stderr.once('data', settle);
} else {
settle();
}
}
}
});
};
return {
async done() {
child?.kill();
future.resolve();
return { stdout, stderr };
},
restart(timeout = common.platformTimeout(10_000)) {
if (!child) {
run();
}
const timer = setTimeout(() => {
if (!future.resolved) {
child.kill();
future.reject(new Error('Timed out waiting for restart'));
}
}, timeout);
return future.promise.finally(() => {
clearTimeout(timer);
});
},
};
}
tmpdir.refresh();
// Create initial file with valid code
const initialContent = `console.log('hello, world');`;
const file = createTmpFile(initialContent, '.mjs');
const { done, restart } = runInBackground({
args: ['--watch', file],
completed: 'Completed running',
shouldFail: true,
});
try {
const { stdout, stderr } = await restart();
assert.strictEqual(stderr, '');
assert.deepStrictEqual(stdout, [
'hello, world',
`Completed running ${inspect(file)}. Waiting for file changes before restarting...`,
]);
// Update file with syntax error
const syntaxErrorContent = `console.log('hello, wor`;
const failedRestart = restart();
writeFileSync(file, syntaxErrorContent);
await sleep(common.platformTimeout(1000));
// Wait for the failed restart
const { stderr: stderr2, stdout: stdout2 } = await failedRestart;
assert.match(stderr2, /SyntaxError: Invalid or unexpected token/);
assert.deepStrictEqual(stdout2, [
`Change detected in ${inspect(file)}`,
`Restarting ${inspect(file)}`,
`Failed running ${inspect(file)}. Waiting for file changes before restarting...`,
]);
const successfulRestart = restart();
writeFileSync(file, `console.log('hello again, world');`);
await sleep(common.platformTimeout(1000));
const { stderr: stderr3, stdout: stdout3 } = await successfulRestart;
// Verify it recovered and ran successfully
assert.strictEqual(stderr3, '');
assert.deepStrictEqual(stdout3, [
`Change detected in ${inspect(file)}`,
`Restarting ${inspect(file)}`,
'hello again, world',
`Completed running ${inspect(file)}. Waiting for file changes before restarting...`,
]);
} finally {
await done();
}