node/test/common/watch.js
Joyee Cheung 7c0995c21b test: split test-runner-watch-mode
The test has been flaky for years and new platforms keep popping
up. As it squeezes too many independent test cases into one file,
split them into individual files to avoid masking regressions
and help only mark the real flaky ones as flaky. This might also
help with the flakiness itself by avoiding updating a shared tmpdir
being watched by differet tests and avoiding running all these
time-consuming tests in one file, which can cause a timeout
on slow machines.

PR-URL: https://github.com/nodejs/node/pull/60391
Refs: https://github.com/nodejs/node/issues/49605
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
2025-10-28 09:08:19 +00:00

174 lines
4.7 KiB
JavaScript

'use strict';
const common = require('./index.js');
const tmpdir = require('./tmpdir.js');
const fixtures = require('./fixtures.js');
const { writeFileSync, readdirSync, readFileSync, renameSync, unlinkSync } = require('node:fs');
const { spawn } = require('node:child_process');
const { once } = require('node:events');
const assert = require('node:assert');
const { setTimeout } = require('node:timers/promises');
function skipIfNoWatch() {
if (common.isIBMi) {
common.skip('IBMi does not support `fs.watch()`');
}
if (common.isAIX) {
common.skip('folder watch capability is limited in AIX.');
}
}
function skipIfNoWatchModeSignals() {
if (common.isWindows) {
common.skip('no signals on Windows');
}
if (common.isIBMi) {
common.skip('IBMi does not support `fs.watch()`');
}
if (common.isAIX) {
common.skip('folder watch capability is limited in AIX.');
}
}
const fixturePaths = {};
const fixtureContent = {};
function refreshForTestRunnerWatch() {
tmpdir.refresh();
const files = readdirSync(fixtures.path('test-runner-watch'));
for (const file of files) {
const src = fixtures.path('test-runner-watch', file);
const dest = tmpdir.resolve(file);
fixturePaths[file] = dest;
fixtureContent[file] = readFileSync(src, 'utf8');
writeFileSync(dest, fixtureContent[file]);
}
}
async function testRunnerWatch({
fileToUpdate,
file,
action = 'update',
fileToCreate,
isolation,
}) {
const ran1 = Promise.withResolvers();
const ran2 = Promise.withResolvers();
const child = spawn(process.execPath,
['--watch', '--test', '--test-reporter=spec',
isolation ? `--test-isolation=${isolation}` : '',
file ? fixturePaths[file] : undefined].filter(Boolean),
{ encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });
let stdout = '';
let currentRun = '';
const runs = [];
child.stdout.on('data', (data) => {
stdout += data.toString();
currentRun += data.toString();
const testRuns = stdout.match(/duration_ms\s\d+/g);
if (testRuns?.length >= 1) ran1.resolve();
if (testRuns?.length >= 2) ran2.resolve();
});
const testUpdate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const content = fixtureContent[fileToUpdate];
const path = fixturePaths[fileToUpdate];
writeFileSync(path, content);
await setTimeout(common.platformTimeout(1000));
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
for (const run of runs) {
assert.match(run, /tests 1/);
assert.match(run, /pass 1/);
assert.match(run, /fail 0/);
assert.match(run, /cancelled 0/);
}
};
const testRename = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
renameSync(fileToRenamePath, newFileNamePath);
await setTimeout(common.platformTimeout(1000));
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
for (const run of runs) {
assert.match(run, /tests 1/);
assert.match(run, /pass 1/);
assert.match(run, /fail 0/);
assert.match(run, /cancelled 0/);
}
};
const testDelete = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
unlinkSync(fileToDeletePath);
await setTimeout(common.platformTimeout(2000));
ran2.resolve();
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
for (const run of runs) {
assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
}
};
const testCreate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const newFilePath = tmpdir.resolve(fileToCreate);
writeFileSync(newFilePath, 'module.exports = {};');
await setTimeout(common.platformTimeout(1000));
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
for (const run of runs) {
assert.match(run, /tests 1/);
assert.match(run, /pass 1/);
assert.match(run, /fail 0/);
assert.match(run, /cancelled 0/);
}
};
action === 'update' && await testUpdate();
action === 'rename' && await testRename();
action === 'delete' && await testDelete();
action === 'create' && await testCreate();
}
module.exports = {
skipIfNoWatch,
skipIfNoWatchModeSignals,
testRunnerWatch,
refreshForTestRunnerWatch,
};