Skip to content

Commit d41cb49

Browse files
Ceres6pmarchini
authored andcommitted
fs: prevent unwanted dependencyOwners removal
Remove files from watcher `dependencyOwners` on file change only if it has no other owners. Co-authored-by: Pietro Marchini <pietro.marchini94@gmail.com> PR-URL: #55565 Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 034505e commit d41cb49

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

lib/internal/watch_mode/files_watcher.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,10 @@ class FilesWatcher extends EventEmitter {
180180
owners.forEach((owner) => {
181181
this.#ownerDependencies.get(owner)?.forEach((dependency) => {
182182
this.#filteredFiles.delete(dependency);
183-
this.#dependencyOwners.delete(dependency);
183+
this.#dependencyOwners.get(dependency)?.delete(owner);
184+
if (this.#dependencyOwners.get(dependency)?.size === 0) {
185+
this.#dependencyOwners.delete(dependency);
186+
}
184187
});
185188
this.#filteredFiles.delete(owner);
186189
this.#dependencyOwners.delete(owner);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Flags: --expose-internals
2+
import * as common from '../common/index.mjs';
3+
import { describe, it } from 'node:test';
4+
import assert from 'node:assert';
5+
import { spawn } from 'node:child_process';
6+
import { writeFileSync } from 'node:fs';
7+
import tmpdir from '../common/tmpdir.js';
8+
9+
if (common.isIBMi)
10+
common.skip('IBMi does not support `fs.watch()`');
11+
12+
if (common.isAIX)
13+
common.skip('folder watch capability is limited in AIX.');
14+
15+
tmpdir.refresh();
16+
17+
// Set up test files and dependencies
18+
const fixtureContent = {
19+
'dependency.js': 'module.exports = {};',
20+
'test.js': `
21+
const test = require('node:test');
22+
require('./dependency.js');
23+
test('first test has ran');`,
24+
'test-2.js': `
25+
const test = require('node:test');
26+
require('./dependency.js');
27+
test('second test has ran');`,
28+
};
29+
30+
const fixturePaths = Object.fromEntries(Object.keys(fixtureContent)
31+
.map((file) => [file, tmpdir.resolve(file)]));
32+
33+
Object.entries(fixtureContent)
34+
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
35+
36+
describe('test runner watch mode with more complex setup', () => {
37+
it('should re-run appropriate tests when dependencies change', async () => {
38+
// Start the test runner in watch mode
39+
const child = spawn(process.execPath,
40+
['--watch', '--test'],
41+
{ encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });
42+
43+
let currentRunOutput = '';
44+
const testRuns = [];
45+
46+
const firstRunCompleted = Promise.withResolvers();
47+
const secondRunCompleted = Promise.withResolvers();
48+
const thirdRunCompleted = Promise.withResolvers();
49+
const fourthRunCompleted = Promise.withResolvers();
50+
51+
child.stdout.on('data', (data) => {
52+
const str = data.toString();
53+
currentRunOutput += str;
54+
55+
if (/duration_ms\s\d+/.test(str)) {
56+
// Test run has completed
57+
testRuns.push(currentRunOutput);
58+
currentRunOutput = '';
59+
switch (testRuns.length) {
60+
case 1:
61+
firstRunCompleted.resolve();
62+
break;
63+
case 2:
64+
secondRunCompleted.resolve();
65+
break;
66+
case 3:
67+
thirdRunCompleted.resolve();
68+
break;
69+
case 4:
70+
fourthRunCompleted.resolve();
71+
break;
72+
}
73+
}
74+
});
75+
76+
// Wait for the initial test run to complete
77+
await firstRunCompleted.promise;
78+
79+
// Modify 'dependency.js' to trigger re-run of both tests
80+
writeFileSync(fixturePaths['dependency.js'], 'module.exports = { modified: true };');
81+
82+
// Wait for the second test run to complete
83+
await secondRunCompleted.promise;
84+
85+
// Modify 'test.js' to trigger re-run of only 'test.js'
86+
writeFileSync(fixturePaths['test.js'], `
87+
const test = require('node:test');
88+
require('./dependency.js');
89+
test('first test has ran again');`);
90+
91+
// Wait for the third test run to complete
92+
await thirdRunCompleted.promise;
93+
94+
// Modify 'dependency.js' again to trigger re-run of both tests
95+
writeFileSync(fixturePaths['dependency.js'], 'module.exports = { modified: true, again: true };');
96+
97+
// Wait for the fourth test run to complete
98+
await fourthRunCompleted.promise;
99+
100+
// Kill the child process
101+
child.kill();
102+
103+
// Analyze the test runs
104+
assert.strictEqual(testRuns.length, 4);
105+
106+
// First test run - Both tests should run
107+
const firstRunOutput = testRuns[0];
108+
assert.match(firstRunOutput, /first test has ran/);
109+
assert.match(firstRunOutput, /second test has ran/);
110+
111+
// Second test run - We have modified 'dependency.js' only, so both tests should re-run
112+
const secondRunOutput = testRuns[1];
113+
assert.match(secondRunOutput, /first test has ran/);
114+
assert.match(secondRunOutput, /second test has ran/);
115+
116+
// Third test run - We have modified 'test.js' only
117+
const thirdRunOutput = testRuns[2];
118+
assert.match(thirdRunOutput, /first test has ran again/);
119+
assert.doesNotMatch(thirdRunOutput, /second test has ran/);
120+
121+
// Fourth test run - We have modified 'dependency.js' again, so both tests should re-run
122+
const fourthRunOutput = testRuns[3];
123+
assert.match(fourthRunOutput, /first test has ran again/);
124+
assert.match(fourthRunOutput, /second test has ran/);
125+
});
126+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Flags: --expose-internals
2+
import * as common from '../common/index.mjs';
3+
import { describe, it } from 'node:test';
4+
import assert from 'node:assert';
5+
import tmpdir from '../common/tmpdir.js';
6+
import watcher from 'internal/watch_mode/files_watcher';
7+
import { writeFileSync } from 'node:fs';
8+
9+
if (common.isIBMi)
10+
common.skip('IBMi does not support `fs.watch()`');
11+
12+
if (common.isAIX)
13+
common.skip('folder watch capability is limited in AIX.');
14+
15+
tmpdir.refresh();
16+
17+
const { FilesWatcher } = watcher;
18+
19+
tmpdir.refresh();
20+
21+
// Set up test files and dependencies
22+
const fixtureContent = {
23+
'dependency.js': 'module.exports = {};',
24+
'test.js': 'require(\'./dependency.js\');',
25+
'test-2.js': 'require(\'./dependency.js\');',
26+
};
27+
28+
const fixturePaths = Object.fromEntries(Object.keys(fixtureContent)
29+
.map((file) => [file, tmpdir.resolve(file)]));
30+
31+
Object.entries(fixtureContent)
32+
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
33+
34+
describe('watch file with shared dependency', () => {
35+
it('should not remove shared dependencies when unfiltering an owner', () => {
36+
const controller = new AbortController();
37+
const watcher = new FilesWatcher({ signal: controller.signal, debounce: 200 });
38+
39+
watcher.on('changed', ({ owners }) => {
40+
assert.strictEqual(owners.size, 2);
41+
assert.ok(owners.has(fixturePaths['test.js']));
42+
assert.ok(owners.has(fixturePaths['test-2.js']));
43+
controller.abort();
44+
});
45+
watcher.filterFile(fixturePaths['test.js']);
46+
watcher.filterFile(fixturePaths['test-2.js']);
47+
watcher.filterFile(fixturePaths['dependency.js'], fixturePaths['test.js']);
48+
watcher.filterFile(fixturePaths['dependency.js'], fixturePaths['test-2.js']);
49+
watcher.unfilterFilesOwnedBy([fixturePaths['test.js']]);
50+
watcher.filterFile(fixturePaths['test.js']);
51+
watcher.filterFile(fixturePaths['dependency.js'], fixturePaths['test.js']);
52+
writeFileSync(fixturePaths['dependency.js'], 'module.exports = { modified: true };');
53+
});
54+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy