Skip to content

Commit 7125931

Browse files
authored
test_runner: handle file rename and deletion under watch mode
Fixes: nodejs#53113 PR-URL: nodejs#53114 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Raz Luvaton <[email protected]>
1 parent 41d90aa commit 7125931

File tree

3 files changed

+88
-22
lines changed

3 files changed

+88
-22
lines changed

lib/internal/test_runner/runner.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {
33
ArrayIsArray,
44
ArrayPrototypeEvery,
55
ArrayPrototypeFilter,
6+
ArrayPrototypeFind,
67
ArrayPrototypeForEach,
78
ArrayPrototypeIncludes,
89
ArrayPrototypeJoin,
@@ -417,7 +418,22 @@ function watchFiles(testFiles, opts) {
417418
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
418419
opts.root.harness.watching = true;
419420

420-
watcher.on('changed', ({ owners }) => {
421+
watcher.on('changed', ({ owners, eventType }) => {
422+
if (eventType === 'rename') {
423+
const updatedTestFiles = createTestFileList();
424+
425+
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
426+
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
427+
428+
// When file renamed
429+
if (newFileName && previousFileName) {
430+
owners = new SafeSet().add(newFileName);
431+
watcher.filterFile(resolve(newFileName), owners);
432+
}
433+
434+
testFiles = updatedTestFiles;
435+
}
436+
421437
watcher.unfilterFilesOwnedBy(owners);
422438
PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => {
423439
if (!owners.has(file)) {

lib/internal/watch_mode/files_watcher.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class FilesWatcher extends EventEmitter {
8080
watcher.handle.close();
8181
}
8282

83-
#onChange(trigger) {
83+
#onChange(trigger, eventType) {
8484
if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) {
8585
return;
8686
}
@@ -93,7 +93,7 @@ class FilesWatcher extends EventEmitter {
9393
clearTimeout(this.#debounceTimer);
9494
this.#debounceTimer = setTimeout(() => {
9595
this.#debounceTimer = null;
96-
this.emit('changed', { owners: this.#debounceOwners });
96+
this.emit('changed', { owners: this.#debounceOwners, eventType });
9797
this.#debounceOwners.clear();
9898
}, this.#debounce).unref();
9999
}
@@ -110,7 +110,7 @@ class FilesWatcher extends EventEmitter {
110110
watcher.on('change', (eventType, fileName) => {
111111
// `fileName` can be `null` if it cannot be determined. See
112112
// https://github.com/nodejs/node/pull/49891#issuecomment-1744673430.
113-
this.#onChange(recursive ? resolve(path, fileName ?? '') : path);
113+
this.#onChange(recursive ? resolve(path, fileName ?? '') : path, eventType);
114114
});
115115
this.#watchers.set(path, { handle: watcher, recursive });
116116
if (recursive) {

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

+68-18
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as common from '../common/index.mjs';
33
import { describe, it } from 'node:test';
44
import assert from 'node:assert';
55
import { spawn } from 'node:child_process';
6-
import { writeFileSync } from 'node:fs';
6+
import { writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';
77
import util from 'internal/util';
88
import tmpdir from '../common/tmpdir.js';
99

@@ -30,7 +30,7 @@ const fixturePaths = Object.keys(fixtureContent)
3030
Object.entries(fixtureContent)
3131
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
3232

33-
async function testWatch({ fileToUpdate, file }) {
33+
async function testWatch({ fileToUpdate, file, action = 'update' }) {
3434
const ran1 = util.createDeferredPromise();
3535
const ran2 = util.createDeferredPromise();
3636
const child = spawn(process.execPath,
@@ -48,22 +48,64 @@ async function testWatch({ fileToUpdate, file }) {
4848
if (testRuns?.length >= 2) ran2.resolve();
4949
});
5050

51-
await ran1.promise;
52-
runs.push(currentRun);
53-
currentRun = '';
54-
const content = fixtureContent[fileToUpdate];
55-
const path = fixturePaths[fileToUpdate];
56-
const interval = setInterval(() => writeFileSync(path, content), common.platformTimeout(1000));
57-
await ran2.promise;
58-
runs.push(currentRun);
59-
clearInterval(interval);
60-
child.kill();
61-
for (const run of runs) {
62-
assert.match(run, /# tests 1/);
63-
assert.match(run, /# pass 1/);
64-
assert.match(run, /# fail 0/);
65-
assert.match(run, /# cancelled 0/);
66-
}
51+
const testUpdate = async () => {
52+
await ran1.promise;
53+
const content = fixtureContent[fileToUpdate];
54+
const path = fixturePaths[fileToUpdate];
55+
const interval = setInterval(() => writeFileSync(path, content), common.platformTimeout(1000));
56+
await ran2.promise;
57+
runs.push(currentRun);
58+
clearInterval(interval);
59+
child.kill();
60+
for (const run of runs) {
61+
assert.match(run, /# tests 1/);
62+
assert.match(run, /# pass 1/);
63+
assert.match(run, /# fail 0/);
64+
assert.match(run, /# cancelled 0/);
65+
}
66+
};
67+
68+
const testRename = async () => {
69+
await ran1.promise;
70+
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
71+
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
72+
const interval = setInterval(() => renameSync(fileToRenamePath, newFileNamePath), common.platformTimeout(1000));
73+
await ran2.promise;
74+
runs.push(currentRun);
75+
clearInterval(interval);
76+
child.kill();
77+
78+
for (const run of runs) {
79+
assert.match(run, /# tests 1/);
80+
assert.match(run, /# pass 1/);
81+
assert.match(run, /# fail 0/);
82+
assert.match(run, /# cancelled 0/);
83+
}
84+
};
85+
86+
const testDelete = async () => {
87+
await ran1.promise;
88+
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
89+
const interval = setInterval(() => {
90+
if (existsSync(fileToDeletePath)) {
91+
unlinkSync(fileToDeletePath);
92+
} else {
93+
ran2.resolve();
94+
}
95+
}, common.platformTimeout(1000));
96+
await ran2.promise;
97+
runs.push(currentRun);
98+
clearInterval(interval);
99+
child.kill();
100+
101+
for (const run of runs) {
102+
assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
103+
}
104+
};
105+
106+
action === 'update' && await testUpdate();
107+
action === 'rename' && await testRename();
108+
action === 'delete' && await testDelete();
67109
}
68110

69111
describe('test runner watch mode', () => {
@@ -82,4 +124,12 @@ describe('test runner watch mode', () => {
82124
it('should support running tests without a file', async () => {
83125
await testWatch({ fileToUpdate: 'test.js' });
84126
});
127+
128+
it('should support a watched test file rename', async () => {
129+
await testWatch({ fileToUpdate: 'test.js', action: 'rename' });
130+
});
131+
132+
it('should not throw when delete a watched test file', async () => {
133+
await testWatch({ fileToUpdate: 'test.js', action: 'delete' });
134+
});
85135
});

0 commit comments

Comments
 (0)