test_runner: ensure test watcher picks up new test files

PR-URL: https://github.com/nodejs/node/pull/54225
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
This commit is contained in:
Pietro Marchini 2024-08-29 10:55:34 +02:00 committed by GitHub
parent 0b6b2a4dc7
commit dcf50f15bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 115 additions and 28 deletions

View File

@ -103,8 +103,7 @@ const kCanceledTests = new SafeSet()
let kResistStopPropagation;
function createTestFileList(patterns) {
const cwd = process.cwd();
function createTestFileList(patterns, cwd) {
const hasUserSuppliedPattern = patterns != null;
if (!patterns || patterns.length === 0) {
patterns = [kDefaultPattern];
@ -361,7 +360,17 @@ function runTestFile(path, filesWatcher, opts) {
env.FORCE_COLOR = '1';
}
const child = spawn(process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio });
const child = spawn(
process.execPath, args,
{
__proto__: null,
signal: t.signal,
encoding: 'utf8',
env,
stdio,
cwd: opts.cwd,
},
);
if (watchMode) {
filesWatcher.runningProcesses.set(path, child);
filesWatcher.watcher.watchChildProcessModules(child, path);
@ -437,7 +446,11 @@ function runTestFile(path, filesWatcher, opts) {
function watchFiles(testFiles, opts) {
const runningProcesses = new SafeMap();
const runningSubtests = new SafeMap();
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal: opts.signal });
const watcherMode = opts.hasFiles ? 'filter' : 'all';
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal });
if (!opts.hasFiles) {
watcher.watchPath(opts.cwd);
}
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
opts.root.harness.watching = true;
@ -455,16 +468,17 @@ function watchFiles(testFiles, opts) {
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
}
// Watch for changes in current filtered files
watcher.on('changed', ({ owners, eventType }) => {
if (!opts.hasFiles && eventType === 'rename') {
const updatedTestFiles = createTestFileList(opts.globPatterns);
if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) {
const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd);
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
testFiles = updatedTestFiles;
// When file renamed
if (newFileName && previousFileName) {
// When file renamed (created / deleted) we need to update the watcher
if (newFileName) {
owners = new SafeSet().add(newFileName);
watcher.filterFile(resolve(newFileName), owners);
}
@ -472,7 +486,6 @@ function watchFiles(testFiles, opts) {
if (!newFileName && previousFileName) {
return; // Avoid rerunning files when file deleted
}
}
if (opts.isolation === 'none') {
@ -611,7 +624,11 @@ function run(options = kEmptyObject) {
setup, // This line can be removed when parseCommandLine() is removed here.
};
const root = createTestTree(rootTestOptions, globalOptions);
let testFiles = files ?? createTestFileList(globPatterns);
// This const should be replaced by a run option in the future.
const cwd = process.cwd();
let testFiles = files ?? createTestFileList(globPatterns, cwd);
if (shard) {
testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
@ -632,6 +649,7 @@ function run(options = kEmptyObject) {
globPatterns,
only,
forceExit,
cwd,
isolation,
};

View File

@ -162,9 +162,7 @@ class FilesWatcher extends EventEmitter {
if (this.#passthroughIPC) {
this.#setupIPC(child);
}
if (this.#mode !== 'filter') {
return;
}
child.on('message', (message) => {
try {
if (ArrayIsArray(message['watch:require'])) {

View File

@ -0,0 +1,4 @@
'use strict';
const test = require('node:test');
test('this should pass');

View File

@ -41,7 +41,7 @@ function refresh() {
const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs');
async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path }) {
async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path, fileToCreate }) {
const ran1 = util.createDeferredPromise();
const ran2 = util.createDeferredPromise();
const args = [runner];
@ -56,7 +56,7 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
child.stdout.on('data', (data) => {
stdout += data.toString();
currentRun += data.toString();
const testRuns = stdout.match(/# duration_ms\s\d+/g);
const testRuns = stdout.match(/duration_ms\s\d+/g);
if (testRuns?.length >= 1) ran1.resolve();
if (testRuns?.length >= 2) ran2.resolve();
});
@ -78,10 +78,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
for (const run of runs) {
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/);
assert.match(run, /tests 1/);
assert.match(run, /pass 1/);
assert.match(run, /fail 0/);
assert.match(run, /cancelled 0/);
}
};
@ -101,10 +101,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
assert.strictEqual(runs.length, 2);
const [firstRun, secondRun] = runs;
assert.match(firstRun, /# tests 1/);
assert.match(firstRun, /# pass 1/);
assert.match(firstRun, /# fail 0/);
assert.match(firstRun, /# cancelled 0/);
assert.match(firstRun, /tests 1/);
assert.match(firstRun, /pass 1/);
assert.match(firstRun, /fail 0/);
assert.match(firstRun, /cancelled 0/);
assert.doesNotMatch(firstRun, /run\(\) is being called recursively/);
if (action === 'rename2') {
@ -112,10 +112,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
return;
}
assert.match(secondRun, /# tests 1/);
assert.match(secondRun, /# pass 1/);
assert.match(secondRun, /# fail 0/);
assert.match(secondRun, /# cancelled 0/);
assert.match(secondRun, /tests 1/);
assert.match(secondRun, /pass 1/);
assert.match(secondRun, /fail 0/);
assert.match(secondRun, /cancelled 0/);
assert.doesNotMatch(secondRun, /run\(\) is being called recursively/);
};
@ -144,10 +144,37 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
}
};
const testCreate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const newFilePath = tmpdir.resolve(fileToCreate);
const interval = setInterval(
() => writeFileSync(
newFilePath,
'module.exports = {};'
),
common.platformTimeout(1000)
);
await ran2.promise;
runs.push(currentRun);
clearInterval(interval);
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 === 'rename2' && await testRename();
action === 'delete' && await testDelete();
action === 'create' && await testCreate();
}
describe('test runner watch mode', () => {
@ -193,4 +220,8 @@ describe('test runner watch mode', () => {
action: 'rename2'
});
});
it('should run new tests when a new file is created in the watched directory', async () => {
await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' });
});
});

View File

@ -37,7 +37,12 @@ function refresh() {
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
}
async function testWatch({ fileToUpdate, file, action = 'update' }) {
async function testWatch({
fileToUpdate,
file,
action = 'update',
fileToCreate,
}) {
const ran1 = util.createDeferredPromise();
const ran2 = util.createDeferredPromise();
const child = spawn(process.execPath,
@ -127,9 +132,36 @@ async function testWatch({ fileToUpdate, file, action = 'update' }) {
}
};
const testCreate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const newFilePath = tmpdir.resolve(fileToCreate);
const interval = setInterval(
() => writeFileSync(
newFilePath,
'module.exports = {};'
),
common.platformTimeout(1000)
);
await ran2.promise;
runs.push(currentRun);
clearInterval(interval);
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();
}
describe('test runner watch mode', () => {
@ -157,4 +189,8 @@ describe('test runner watch mode', () => {
it('should not throw when delete a watched test file', { skip: common.isAIX }, async () => {
await testWatch({ fileToUpdate: 'test.js', action: 'delete' });
});
it('should run new tests when a new file is created in the watched directory', async () => {
await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' });
});
});