mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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:
parent
0b6b2a4dc7
commit
dcf50f15bc
5 changed files with 115 additions and 28 deletions
|
@ -103,8 +103,7 @@ const kCanceledTests = new SafeSet()
|
||||||
|
|
||||||
let kResistStopPropagation;
|
let kResistStopPropagation;
|
||||||
|
|
||||||
function createTestFileList(patterns) {
|
function createTestFileList(patterns, cwd) {
|
||||||
const cwd = process.cwd();
|
|
||||||
const hasUserSuppliedPattern = patterns != null;
|
const hasUserSuppliedPattern = patterns != null;
|
||||||
if (!patterns || patterns.length === 0) {
|
if (!patterns || patterns.length === 0) {
|
||||||
patterns = [kDefaultPattern];
|
patterns = [kDefaultPattern];
|
||||||
|
@ -361,7 +360,17 @@ function runTestFile(path, filesWatcher, opts) {
|
||||||
env.FORCE_COLOR = '1';
|
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) {
|
if (watchMode) {
|
||||||
filesWatcher.runningProcesses.set(path, child);
|
filesWatcher.runningProcesses.set(path, child);
|
||||||
filesWatcher.watcher.watchChildProcessModules(child, path);
|
filesWatcher.watcher.watchChildProcessModules(child, path);
|
||||||
|
@ -437,7 +446,11 @@ function runTestFile(path, filesWatcher, opts) {
|
||||||
function watchFiles(testFiles, opts) {
|
function watchFiles(testFiles, opts) {
|
||||||
const runningProcesses = new SafeMap();
|
const runningProcesses = new SafeMap();
|
||||||
const runningSubtests = 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 };
|
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
|
||||||
opts.root.harness.watching = true;
|
opts.root.harness.watching = true;
|
||||||
|
|
||||||
|
@ -455,16 +468,17 @@ function watchFiles(testFiles, opts) {
|
||||||
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
|
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for changes in current filtered files
|
||||||
watcher.on('changed', ({ owners, eventType }) => {
|
watcher.on('changed', ({ owners, eventType }) => {
|
||||||
if (!opts.hasFiles && eventType === 'rename') {
|
if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) {
|
||||||
const updatedTestFiles = createTestFileList(opts.globPatterns);
|
const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd);
|
||||||
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
|
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
|
||||||
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
|
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
|
||||||
|
|
||||||
testFiles = updatedTestFiles;
|
testFiles = updatedTestFiles;
|
||||||
|
|
||||||
// When file renamed
|
// When file renamed (created / deleted) we need to update the watcher
|
||||||
if (newFileName && previousFileName) {
|
if (newFileName) {
|
||||||
owners = new SafeSet().add(newFileName);
|
owners = new SafeSet().add(newFileName);
|
||||||
watcher.filterFile(resolve(newFileName), owners);
|
watcher.filterFile(resolve(newFileName), owners);
|
||||||
}
|
}
|
||||||
|
@ -472,7 +486,6 @@ function watchFiles(testFiles, opts) {
|
||||||
if (!newFileName && previousFileName) {
|
if (!newFileName && previousFileName) {
|
||||||
return; // Avoid rerunning files when file deleted
|
return; // Avoid rerunning files when file deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.isolation === 'none') {
|
if (opts.isolation === 'none') {
|
||||||
|
@ -611,7 +624,11 @@ function run(options = kEmptyObject) {
|
||||||
setup, // This line can be removed when parseCommandLine() is removed here.
|
setup, // This line can be removed when parseCommandLine() is removed here.
|
||||||
};
|
};
|
||||||
const root = createTestTree(rootTestOptions, globalOptions);
|
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) {
|
if (shard) {
|
||||||
testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
|
testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
|
||||||
|
@ -632,6 +649,7 @@ function run(options = kEmptyObject) {
|
||||||
globPatterns,
|
globPatterns,
|
||||||
only,
|
only,
|
||||||
forceExit,
|
forceExit,
|
||||||
|
cwd,
|
||||||
isolation,
|
isolation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -162,9 +162,7 @@ class FilesWatcher extends EventEmitter {
|
||||||
if (this.#passthroughIPC) {
|
if (this.#passthroughIPC) {
|
||||||
this.#setupIPC(child);
|
this.#setupIPC(child);
|
||||||
}
|
}
|
||||||
if (this.#mode !== 'filter') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
child.on('message', (message) => {
|
child.on('message', (message) => {
|
||||||
try {
|
try {
|
||||||
if (ArrayIsArray(message['watch:require'])) {
|
if (ArrayIsArray(message['watch:require'])) {
|
||||||
|
|
4
test/fixtures/test-runner/cwd/test.cjs
vendored
Normal file
4
test/fixtures/test-runner/cwd/test.cjs
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict';
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
test('this should pass');
|
|
@ -41,7 +41,7 @@ function refresh() {
|
||||||
|
|
||||||
const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs');
|
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 ran1 = util.createDeferredPromise();
|
||||||
const ran2 = util.createDeferredPromise();
|
const ran2 = util.createDeferredPromise();
|
||||||
const args = [runner];
|
const args = [runner];
|
||||||
|
@ -56,7 +56,7 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
currentRun += 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 >= 1) ran1.resolve();
|
||||||
if (testRuns?.length >= 2) ran2.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) {
|
for (const run of runs) {
|
||||||
assert.doesNotMatch(run, /run\(\) is being called recursively/);
|
assert.doesNotMatch(run, /run\(\) is being called recursively/);
|
||||||
assert.match(run, /# tests 1/);
|
assert.match(run, /tests 1/);
|
||||||
assert.match(run, /# pass 1/);
|
assert.match(run, /pass 1/);
|
||||||
assert.match(run, /# fail 0/);
|
assert.match(run, /fail 0/);
|
||||||
assert.match(run, /# cancelled 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);
|
assert.strictEqual(runs.length, 2);
|
||||||
|
|
||||||
const [firstRun, secondRun] = runs;
|
const [firstRun, secondRun] = runs;
|
||||||
assert.match(firstRun, /# tests 1/);
|
assert.match(firstRun, /tests 1/);
|
||||||
assert.match(firstRun, /# pass 1/);
|
assert.match(firstRun, /pass 1/);
|
||||||
assert.match(firstRun, /# fail 0/);
|
assert.match(firstRun, /fail 0/);
|
||||||
assert.match(firstRun, /# cancelled 0/);
|
assert.match(firstRun, /cancelled 0/);
|
||||||
assert.doesNotMatch(firstRun, /run\(\) is being called recursively/);
|
assert.doesNotMatch(firstRun, /run\(\) is being called recursively/);
|
||||||
|
|
||||||
if (action === 'rename2') {
|
if (action === 'rename2') {
|
||||||
|
@ -112,10 +112,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.match(secondRun, /# tests 1/);
|
assert.match(secondRun, /tests 1/);
|
||||||
assert.match(secondRun, /# pass 1/);
|
assert.match(secondRun, /pass 1/);
|
||||||
assert.match(secondRun, /# fail 0/);
|
assert.match(secondRun, /fail 0/);
|
||||||
assert.match(secondRun, /# cancelled 0/);
|
assert.match(secondRun, /cancelled 0/);
|
||||||
assert.doesNotMatch(secondRun, /run\(\) is being called recursively/);
|
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 === 'update' && await testUpdate();
|
||||||
action === 'rename' && await testRename();
|
action === 'rename' && await testRename();
|
||||||
action === 'rename2' && await testRename();
|
action === 'rename2' && await testRename();
|
||||||
action === 'delete' && await testDelete();
|
action === 'delete' && await testDelete();
|
||||||
|
action === 'create' && await testCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('test runner watch mode', () => {
|
describe('test runner watch mode', () => {
|
||||||
|
@ -193,4 +220,8 @@ describe('test runner watch mode', () => {
|
||||||
action: 'rename2'
|
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' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,7 +37,12 @@ function refresh() {
|
||||||
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
|
.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 ran1 = util.createDeferredPromise();
|
||||||
const ran2 = util.createDeferredPromise();
|
const ran2 = util.createDeferredPromise();
|
||||||
const child = spawn(process.execPath,
|
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 === 'update' && await testUpdate();
|
||||||
action === 'rename' && await testRename();
|
action === 'rename' && await testRename();
|
||||||
action === 'delete' && await testDelete();
|
action === 'delete' && await testDelete();
|
||||||
|
action === 'create' && await testCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('test runner watch mode', () => {
|
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 () => {
|
it('should not throw when delete a watched test file', { skip: common.isAIX }, async () => {
|
||||||
await testWatch({ fileToUpdate: 'test.js', action: 'delete' });
|
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' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue