mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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>
144 lines
4.4 KiB
JavaScript
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();
|
|
}
|