Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a4bebf8

Browse filesBrowse files
pmarchiniRafaelGSS
authored andcommitted
test_runner: ensure test watcher picks up new test files
PR-URL: #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>
1 parent a0be95e commit a4bebf8
Copy full SHA for a4bebf8

File tree

Expand file treeCollapse file tree

5 files changed

+115
-28
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+115
-28
lines changed
Open diff view settings
Collapse file

‎lib/internal/test_runner/runner.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js
+28-10Lines 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

Collapse file

‎lib/internal/watch_mode/files_watcher.js‎

Copy file name to clipboardExpand all lines: lib/internal/watch_mode/files_watcher.js
+1-3Lines 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'])) {
Collapse file
+4Lines 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');
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-runner-run-watch.mjs
+45-14Lines 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
});
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-runner-watch-mode.mjs
+37-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.