Skip to content

Commit a4bebf8

Browse files
pmarchiniRafaelGSS
authored andcommitted
test_runner: ensure test watcher picks up new test files
PR-URL: #54225 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Jake Yuesong Li <[email protected]>
1 parent a0be95e commit a4bebf8

File tree

5 files changed

+115
-28
lines changed

5 files changed

+115
-28
lines changed

lib/internal/test_runner/runner.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ const kCanceledTests = new SafeSet()
103103

104104
let kResistStopPropagation;
105105

106-
function createTestFileList(patterns) {
107-
const cwd = process.cwd();
106+
function createTestFileList(patterns, cwd) {
108107
const hasUserSuppliedPattern = patterns != null;
109108
if (!patterns || patterns.length === 0) {
110109
patterns = [kDefaultPattern];
@@ -361,7 +360,17 @@ function runTestFile(path, filesWatcher, opts) {
361360
env.FORCE_COLOR = '1';
362361
}
363362

364-
const child = spawn(process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio });
363+
const child = spawn(
364+
process.execPath, args,
365+
{
366+
__proto__: null,
367+
signal: t.signal,
368+
encoding: 'utf8',
369+
env,
370+
stdio,
371+
cwd: opts.cwd,
372+
},
373+
);
365374
if (watchMode) {
366375
filesWatcher.runningProcesses.set(path, child);
367376
filesWatcher.watcher.watchChildProcessModules(child, path);
@@ -437,7 +446,11 @@ function runTestFile(path, filesWatcher, opts) {
437446
function watchFiles(testFiles, opts) {
438447
const runningProcesses = new SafeMap();
439448
const runningSubtests = new SafeMap();
440-
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal: opts.signal });
449+
const watcherMode = opts.hasFiles ? 'filter' : 'all';
450+
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal });
451+
if (!opts.hasFiles) {
452+
watcher.watchPath(opts.cwd);
453+
}
441454
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
442455
opts.root.harness.watching = true;
443456

@@ -455,24 +468,24 @@ function watchFiles(testFiles, opts) {
455468
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
456469
}
457470

471+
// Watch for changes in current filtered files
458472
watcher.on('changed', ({ owners, eventType }) => {
459-
if (!opts.hasFiles && eventType === 'rename') {
460-
const updatedTestFiles = createTestFileList(opts.globPatterns);
473+
if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) {
474+
const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd);
461475
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
462476
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
463477

464478
testFiles = updatedTestFiles;
465479

466-
// When file renamed
467-
if (newFileName && previousFileName) {
480+
// When file renamed (created / deleted) we need to update the watcher
481+
if (newFileName) {
468482
owners = new SafeSet().add(newFileName);
469483
watcher.filterFile(resolve(newFileName), owners);
470484
}
471485

472486
if (!newFileName && previousFileName) {
473487
return; // Avoid rerunning files when file deleted
474488
}
475-
476489
}
477490

478491
if (opts.isolation === 'none') {
@@ -611,7 +624,11 @@ function run(options = kEmptyObject) {
611624
setup, // This line can be removed when parseCommandLine() is removed here.
612625
};
613626
const root = createTestTree(rootTestOptions, globalOptions);
614-
let testFiles = files ?? createTestFileList(globPatterns);
627+
628+
// This const should be replaced by a run option in the future.
629+
const cwd = process.cwd();
630+
631+
let testFiles = files ?? createTestFileList(globPatterns, cwd);
615632

616633
if (shard) {
617634
testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
@@ -632,6 +649,7 @@ function run(options = kEmptyObject) {
632649
globPatterns,
633650
only,
634651
forceExit,
652+
cwd,
635653
isolation,
636654
};
637655

lib/internal/watch_mode/files_watcher.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,7 @@ class FilesWatcher extends EventEmitter {
162162
if (this.#passthroughIPC) {
163163
this.#setupIPC(child);
164164
}
165-
if (this.#mode !== 'filter') {
166-
return;
167-
}
165+
168166
child.on('message', (message) => {
169167
try {
170168
if (ArrayIsArray(message['watch:require'])) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
'use strict';
2+
const test = require('node:test');
3+
4+
test('this should pass');

test/parallel/test-runner-run-watch.mjs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function refresh() {
4141

4242
const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs');
4343

44-
async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path }) {
44+
async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path, fileToCreate }) {
4545
const ran1 = util.createDeferredPromise();
4646
const ran2 = util.createDeferredPromise();
4747
const args = [runner];
@@ -56,7 +56,7 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
5656
child.stdout.on('data', (data) => {
5757
stdout += data.toString();
5858
currentRun += data.toString();
59-
const testRuns = stdout.match(/# duration_ms\s\d+/g);
59+
const testRuns = stdout.match(/duration_ms\s\d+/g);
6060
if (testRuns?.length >= 1) ran1.resolve();
6161
if (testRuns?.length >= 2) ran2.resolve();
6262
});
@@ -78,10 +78,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
7878

7979
for (const run of runs) {
8080
assert.doesNotMatch(run, /run\(\) is being called recursively/);
81-
assert.match(run, /# tests 1/);
82-
assert.match(run, /# pass 1/);
83-
assert.match(run, /# fail 0/);
84-
assert.match(run, /# cancelled 0/);
81+
assert.match(run, /tests 1/);
82+
assert.match(run, /pass 1/);
83+
assert.match(run, /fail 0/);
84+
assert.match(run, /cancelled 0/);
8585
}
8686
};
8787

@@ -101,21 +101,21 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
101101
assert.strictEqual(runs.length, 2);
102102

103103
const [firstRun, secondRun] = runs;
104-
assert.match(firstRun, /# tests 1/);
105-
assert.match(firstRun, /# pass 1/);
106-
assert.match(firstRun, /# fail 0/);
107-
assert.match(firstRun, /# cancelled 0/);
104+
assert.match(firstRun, /tests 1/);
105+
assert.match(firstRun, /pass 1/);
106+
assert.match(firstRun, /fail 0/);
107+
assert.match(firstRun, /cancelled 0/);
108108
assert.doesNotMatch(firstRun, /run\(\) is being called recursively/);
109109

110110
if (action === 'rename2') {
111111
assert.match(secondRun, /MODULE_NOT_FOUND/);
112112
return;
113113
}
114114

115-
assert.match(secondRun, /# tests 1/);
116-
assert.match(secondRun, /# pass 1/);
117-
assert.match(secondRun, /# fail 0/);
118-
assert.match(secondRun, /# cancelled 0/);
115+
assert.match(secondRun, /tests 1/);
116+
assert.match(secondRun, /pass 1/);
117+
assert.match(secondRun, /fail 0/);
118+
assert.match(secondRun, /cancelled 0/);
119119
assert.doesNotMatch(secondRun, /run\(\) is being called recursively/);
120120
};
121121

@@ -144,10 +144,37 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p
144144
}
145145
};
146146

147+
const testCreate = async () => {
148+
await ran1.promise;
149+
runs.push(currentRun);
150+
currentRun = '';
151+
const newFilePath = tmpdir.resolve(fileToCreate);
152+
const interval = setInterval(
153+
() => writeFileSync(
154+
newFilePath,
155+
'module.exports = {};'
156+
),
157+
common.platformTimeout(1000)
158+
);
159+
await ran2.promise;
160+
runs.push(currentRun);
161+
clearInterval(interval);
162+
child.kill();
163+
await once(child, 'exit');
164+
165+
for (const run of runs) {
166+
assert.match(run, /tests 1/);
167+
assert.match(run, /pass 1/);
168+
assert.match(run, /fail 0/);
169+
assert.match(run, /cancelled 0/);
170+
}
171+
};
172+
147173
action === 'update' && await testUpdate();
148174
action === 'rename' && await testRename();
149175
action === 'rename2' && await testRename();
150176
action === 'delete' && await testDelete();
177+
action === 'create' && await testCreate();
151178
}
152179

153180
describe('test runner watch mode', () => {
@@ -193,4 +220,8 @@ describe('test runner watch mode', () => {
193220
action: 'rename2'
194221
});
195222
});
223+
224+
it('should run new tests when a new file is created in the watched directory', async () => {
225+
await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' });
226+
});
196227
});

test/parallel/test-runner-watch-mode.mjs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ function refresh() {
3737
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
3838
}
3939

40-
async function testWatch({ fileToUpdate, file, action = 'update' }) {
40+
async function testWatch({
41+
fileToUpdate,
42+
file,
43+
action = 'update',
44+
fileToCreate,
45+
}) {
4146
const ran1 = util.createDeferredPromise();
4247
const ran2 = util.createDeferredPromise();
4348
const child = spawn(process.execPath,
@@ -127,9 +132,36 @@ async function testWatch({ fileToUpdate, file, action = 'update' }) {
127132
}
128133
};
129134

135+
const testCreate = async () => {
136+
await ran1.promise;
137+
runs.push(currentRun);
138+
currentRun = '';
139+
const newFilePath = tmpdir.resolve(fileToCreate);
140+
const interval = setInterval(
141+
() => writeFileSync(
142+
newFilePath,
143+
'module.exports = {};'
144+
),
145+
common.platformTimeout(1000)
146+
);
147+
await ran2.promise;
148+
runs.push(currentRun);
149+
clearInterval(interval);
150+
child.kill();
151+
await once(child, 'exit');
152+
153+
for (const run of runs) {
154+
assert.match(run, /tests 1/);
155+
assert.match(run, /pass 1/);
156+
assert.match(run, /fail 0/);
157+
assert.match(run, /cancelled 0/);
158+
}
159+
};
160+
130161
action === 'update' && await testUpdate();
131162
action === 'rename' && await testRename();
132163
action === 'delete' && await testDelete();
164+
action === 'create' && await testCreate();
133165
}
134166

135167
describe('test runner watch mode', () => {
@@ -157,4 +189,8 @@ describe('test runner watch mode', () => {
157189
it('should not throw when delete a watched test file', { skip: common.isAIX }, async () => {
158190
await testWatch({ fileToUpdate: 'test.js', action: 'delete' });
159191
});
192+
193+
it('should run new tests when a new file is created in the watched directory', async () => {
194+
await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' });
195+
});
160196
});

0 commit comments

Comments
 (0)