This test contains too many independent test cases and as a result, marking it as flaky on all major platforms means actual regressions could be covered up, and it's constantly making the CI orange and requires extra resuming on the flaked platforms which is still not great. Split it into individual files so that the actual flake can be identified out of the monolith. PR-URL: https://github.com/nodejs/node/pull/60653 Refs: https://github.com/nodejs/node/issues/54534 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
247 lines
6.2 KiB
JavaScript
247 lines
6.2 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 performFileOperation(operation, useRunApi, timeout = 1000) {
|
|
if (useRunApi) {
|
|
const interval = setInterval(() => {
|
|
operation();
|
|
clearInterval(interval);
|
|
}, common.platformTimeout(timeout));
|
|
} else {
|
|
operation();
|
|
await setTimeout(common.platformTimeout(timeout));
|
|
}
|
|
}
|
|
|
|
function assertTestOutput(run, shouldCheckRecursion = false) {
|
|
if (shouldCheckRecursion) {
|
|
assert.doesNotMatch(run, /run\(\) is being called recursively/);
|
|
}
|
|
assert.match(run, /tests 1/);
|
|
assert.match(run, /pass 1/);
|
|
assert.match(run, /fail 0/);
|
|
assert.match(run, /cancelled 0/);
|
|
}
|
|
|
|
async function testRunnerWatch({
|
|
fileToUpdate,
|
|
file,
|
|
action = 'update',
|
|
fileToCreate,
|
|
isolation,
|
|
useRunApi = false,
|
|
cwd = tmpdir.path,
|
|
runnerCwd,
|
|
}) {
|
|
const ran1 = Promise.withResolvers();
|
|
const ran2 = Promise.withResolvers();
|
|
|
|
let args;
|
|
if (useRunApi) {
|
|
// Use the fixture that calls run() API
|
|
const runner = fixtures.path('test-runner-watch.mjs');
|
|
args = [runner];
|
|
if (file) args.push('--file', file);
|
|
if (runnerCwd) args.push('--cwd', runnerCwd);
|
|
if (isolation) args.push('--isolation', isolation);
|
|
} else {
|
|
// Use CLI --watch --test flags
|
|
args = ['--watch', '--test', '--test-reporter=spec',
|
|
isolation ? `--test-isolation=${isolation}` : '',
|
|
file ? fixturePaths[file] : undefined].filter(Boolean);
|
|
}
|
|
|
|
const child = spawn(process.execPath, args,
|
|
{ encoding: 'utf8', stdio: 'pipe', cwd });
|
|
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];
|
|
|
|
if (useRunApi) {
|
|
const interval = setInterval(
|
|
() => writeFileSync(path, content),
|
|
common.platformTimeout(1000),
|
|
);
|
|
await ran2.promise;
|
|
clearInterval(interval);
|
|
} else {
|
|
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) {
|
|
assertTestOutput(run, useRunApi);
|
|
}
|
|
};
|
|
|
|
const testRename = async () => {
|
|
await ran1.promise;
|
|
runs.push(currentRun);
|
|
currentRun = '';
|
|
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
|
|
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
|
|
|
|
await performFileOperation(
|
|
() => renameSync(fileToRenamePath, newFileNamePath),
|
|
useRunApi,
|
|
);
|
|
await ran2.promise;
|
|
|
|
runs.push(currentRun);
|
|
child.kill();
|
|
await once(child, 'exit');
|
|
|
|
assert.strictEqual(runs.length, 2);
|
|
|
|
const [firstRun, secondRun] = runs;
|
|
assertTestOutput(firstRun, useRunApi);
|
|
|
|
if (action === 'rename2') {
|
|
assert.match(secondRun, /MODULE_NOT_FOUND/);
|
|
return;
|
|
}
|
|
|
|
assertTestOutput(secondRun, useRunApi);
|
|
};
|
|
|
|
const testDelete = async () => {
|
|
await ran1.promise;
|
|
runs.push(currentRun);
|
|
currentRun = '';
|
|
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
|
|
|
|
if (useRunApi) {
|
|
const { existsSync } = require('node:fs');
|
|
const interval = setInterval(() => {
|
|
if (existsSync(fileToDeletePath)) {
|
|
unlinkSync(fileToDeletePath);
|
|
} else {
|
|
ran2.resolve();
|
|
clearInterval(interval);
|
|
}
|
|
}, common.platformTimeout(1000));
|
|
await ran2.promise;
|
|
} else {
|
|
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);
|
|
|
|
await performFileOperation(
|
|
() => writeFileSync(newFilePath, 'module.exports = {};'),
|
|
useRunApi,
|
|
);
|
|
await ran2.promise;
|
|
|
|
runs.push(currentRun);
|
|
child.kill();
|
|
await once(child, 'exit');
|
|
|
|
for (const run of runs) {
|
|
assertTestOutput(run, false);
|
|
}
|
|
};
|
|
|
|
action === 'update' && await testUpdate();
|
|
action === 'rename' && await testRename();
|
|
action === 'rename2' && await testRename();
|
|
action === 'delete' && await testDelete();
|
|
action === 'create' && await testCreate();
|
|
|
|
return runs;
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
skipIfNoWatch,
|
|
skipIfNoWatchModeSignals,
|
|
testRunnerWatch,
|
|
refreshForTestRunnerWatch,
|
|
fixtureContent,
|
|
fixturePaths,
|
|
};
|