Skip to content

Commit 1212eca

Browse files
authored
test_runner: fix delete test file cause dependency file not watched
When a watched test file is being deleted then the referenced dependency file(s) will be updated incorrect when `unfilterFilesOwnedBy` method is called, which will cause tests not being rerun when its referenced dependency changed. To prevent this case, we can simply `return` when we detect a watched test file being deleted. PR-URL: #53533 Refs: #53114 Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent c1ec099 commit 1212eca

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

lib/internal/test_runner/runner.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,18 @@ function watchFiles(testFiles, opts) {
424424
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
425425
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
426426

427+
testFiles = updatedTestFiles;
428+
427429
// When file renamed
428430
if (newFileName && previousFileName) {
429431
owners = new SafeSet().add(newFileName);
430432
watcher.filterFile(resolve(newFileName), owners);
431433
}
432434

433-
testFiles = updatedTestFiles;
435+
if (!newFileName && previousFileName) {
436+
return; // Avoid rerunning files when file deleted
437+
}
438+
434439
}
435440

436441
watcher.unfilterFilesOwnedBy(owners);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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, unlinkSync } from 'node:fs';
7+
import util from 'internal/util';
8+
import tmpdir from '../common/tmpdir.js';
9+
10+
if (common.isIBMi)
11+
common.skip('IBMi does not support `fs.watch()`');
12+
13+
if (common.isAIX)
14+
common.skip('folder watch capability is limited in AIX.');
15+
16+
tmpdir.refresh();
17+
18+
// This test updates these files repeatedly,
19+
// Reading them from disk is unreliable due to race conditions.
20+
const fixtureContent = {
21+
'dependency.js': 'module.exports = {};',
22+
'dependency.mjs': 'export const a = 1;',
23+
// Test 1
24+
'test.js': `
25+
const test = require('node:test');
26+
require('./dependency.js');
27+
import('./dependency.mjs');
28+
import('data:text/javascript,');
29+
test('first test has ran');`,
30+
// Test 2
31+
'test-2.mjs': `
32+
import test from 'node:test';
33+
import './dependency.js';
34+
import { a } from './dependency.mjs';
35+
import 'data:text/javascript,';
36+
test('second test has ran');`,
37+
// Test file to be deleted
38+
'test-to-delete.mjs': `
39+
import test from 'node:test';
40+
import './dependency.js';
41+
import { a } from './dependency.mjs';
42+
import 'data:text/javascript,';
43+
test('test to delete has ran');`,
44+
};
45+
46+
const fixturePaths = Object.fromEntries(Object.keys(fixtureContent)
47+
.map((file) => [file, tmpdir.resolve(file)]));
48+
49+
Object.entries(fixtureContent)
50+
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
51+
52+
describe('test runner watch mode with more complex setup', () => {
53+
it('should run tests when a dependency changed after a watched test file being deleted', async () => {
54+
const ran1 = util.createDeferredPromise();
55+
const ran2 = util.createDeferredPromise();
56+
const child = spawn(process.execPath,
57+
['--watch', '--test'],
58+
{ encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });
59+
let stdout = '';
60+
let currentRun = '';
61+
const runs = [];
62+
63+
child.stdout.on('data', (data) => {
64+
stdout += data.toString();
65+
currentRun += data.toString();
66+
const testRuns = stdout.match(/# duration_ms\s\d+/g);
67+
68+
if (testRuns?.length >= 2) {
69+
ran2.resolve();
70+
return;
71+
}
72+
if (testRuns?.length >= 1) ran1.resolve();
73+
});
74+
75+
await ran1.promise;
76+
runs.push(currentRun);
77+
currentRun = '';
78+
const fileToDeletePathLocal = tmpdir.resolve('test-to-delete.mjs');
79+
unlinkSync(fileToDeletePathLocal);
80+
81+
const content = fixtureContent['dependency.mjs'];
82+
const path = fixturePaths['dependency.mjs'];
83+
const interval = setInterval(() => writeFileSync(path, content), common.platformTimeout(1000));
84+
await ran2.promise;
85+
runs.push(currentRun);
86+
currentRun = '';
87+
clearInterval(interval);
88+
child.kill();
89+
90+
assert.strictEqual(runs.length, 2);
91+
92+
const [firstRun, secondRun] = runs;
93+
94+
assert.match(firstRun, /# tests 3/);
95+
assert.match(firstRun, /# pass 3/);
96+
assert.match(firstRun, /# fail 0/);
97+
assert.match(firstRun, /# cancelled 0/);
98+
99+
assert.match(secondRun, /# tests 2/);
100+
assert.match(secondRun, /# pass 2/);
101+
assert.match(secondRun, /# fail 0/);
102+
assert.match(secondRun, /# cancelled 0/);
103+
});
104+
});

0 commit comments

Comments
 (0)