node/test/parallel/test-watch-mode-files_watcher.mjs
Jason Zhang 8456a12459
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
watch: check parent and child path properly
Co-authored-by: Jake Yuesong Li <jake.yuesong@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/57425
Fixes: https://github.com/nodejs/node/issues/57422
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2025-04-06 09:48:53 +02:00

214 lines
7.5 KiB
JavaScript

// Flags: --expose-internals
import * as common from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import tmpdir from '../common/tmpdir.js';
import path from 'node:path';
import assert from 'node:assert';
import process from 'node:process';
import { describe, it, beforeEach, afterEach } from 'node:test';
import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
import { createInterface } from 'node:readline';
import { setTimeout } from 'node:timers/promises';
import { once } from 'node:events';
import { spawn } from 'node:child_process';
import watcher from 'internal/watch_mode/files_watcher';
if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
const supportsRecursiveWatching = common.isMacOS || common.isWindows;
const { FilesWatcher } = watcher;
tmpdir.refresh();
describe('watch mode file watcher', () => {
let watcher;
let changesCount;
beforeEach(() => {
changesCount = 0;
watcher = new FilesWatcher({ debounce: 100 });
watcher.on('changed', () => changesCount++);
});
afterEach(() => watcher.clear());
let counter = 0;
function writeAndWaitForChanges(watcher, file) {
return new Promise((resolve) => {
const interval = setInterval(() => writeFileSync(file, `write ${counter++}`), 100);
watcher.once('changed', () => {
clearInterval(interval);
resolve();
});
});
}
it('should watch changed files', async () => {
const file = tmpdir.resolve('file1');
writeFileSync(file, 'written');
watcher.filterFile(file);
await writeAndWaitForChanges(watcher, file);
assert.strictEqual(changesCount, 1);
});
it('should watch changed files with same prefix path string', async () => {
mkdirSync(tmpdir.resolve('subdir'));
mkdirSync(tmpdir.resolve('sub'));
const file1 = tmpdir.resolve('subdir', 'file1.mjs');
const file2 = tmpdir.resolve('sub', 'file2.mjs');
writeFileSync(file2, 'export const hello = () => { return "hello world"; };');
writeFileSync(file1, 'import { hello } from "../sub/file2.mjs"; console.log(hello());');
const child = spawn(process.execPath,
['--watch', file1],
{ stdio: ['ignore', 'pipe', 'ignore'] });
let completeCount = 0;
for await (const line of createInterface(child.stdout)) {
if (!line.startsWith('Completed running')) {
continue;
}
completeCount++;
if (completeCount === 1) {
appendFileSync(file1, '\n // append 1');
}
// The file is reloaded due to file watching
if (completeCount === 2) {
child.kill();
}
}
});
it('should debounce changes', async () => {
const file = tmpdir.resolve('file2');
writeFileSync(file, 'written');
watcher.filterFile(file);
await writeAndWaitForChanges(watcher, file);
writeFileSync(file, '1');
writeFileSync(file, '2');
writeFileSync(file, '3');
writeFileSync(file, '4');
await setTimeout(200); // debounce * 2
writeFileSync(file, '5');
const changed = once(watcher, 'changed');
writeFileSync(file, 'after');
await changed;
// Unfortunately testing that changesCount === 2 is flaky
assert.ok(changesCount < 5);
});
it('should debounce changes on multiple files', async () => {
const files = [];
for (let i = 0; i < 10; i++) {
const file = tmpdir.resolve(`file-debounced-${i}`);
writeFileSync(file, 'written');
watcher.filterFile(file);
files.push(file);
}
files.forEach((file) => writeFileSync(file, '1'));
files.forEach((file) => writeFileSync(file, '2'));
files.forEach((file) => writeFileSync(file, '3'));
files.forEach((file) => writeFileSync(file, '4'));
await setTimeout(200); // debounce * 2
files.forEach((file) => writeFileSync(file, '5'));
const changed = once(watcher, 'changed');
files.forEach((file) => writeFileSync(file, 'after'));
await changed;
// Unfortunately testing that changesCount === 2 is flaky
assert.ok(changesCount < 5);
});
it('should ignore files in watched directory if they are not filtered',
{ skip: !supportsRecursiveWatching }, async () => {
watcher.on('changed', common.mustNotCall());
watcher.watchPath(tmpdir.path);
writeFileSync(tmpdir.resolve('file3'), '1');
// Wait for this long to make sure changes are not triggered
await setTimeout(1000);
});
it('should allow clearing filters', async () => {
const file = tmpdir.resolve('file4');
writeFileSync(file, 'written');
watcher.filterFile(file);
await writeAndWaitForChanges(watcher, file);
writeFileSync(file, '1');
assert.strictEqual(changesCount, 1);
watcher.clearFileFilters();
writeFileSync(file, '2');
// Wait for this long to make sure changes are triggered only once
await setTimeout(1000);
assert.strictEqual(changesCount, 1);
});
it('should watch all files in watched path when in "all" mode',
{ skip: !supportsRecursiveWatching }, async () => {
watcher = new FilesWatcher({ debounce: 100, mode: 'all' });
watcher.on('changed', () => changesCount++);
const file = tmpdir.resolve('file5');
watcher.watchPath(tmpdir.path);
const changed = once(watcher, 'changed');
await setTimeout(common.platformTimeout(100)); // avoid throttling
writeFileSync(file, 'changed');
await changed;
assert.strictEqual(changesCount, 1);
});
it('should ruse existing watcher if it exists',
{ skip: !supportsRecursiveWatching }, () => {
assert.deepStrictEqual(watcher.watchedPaths, []);
watcher.watchPath(tmpdir.path);
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
watcher.watchPath(tmpdir.path);
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
});
it('should ruse existing watcher of a parent directory',
{ skip: !supportsRecursiveWatching }, () => {
assert.deepStrictEqual(watcher.watchedPaths, []);
watcher.watchPath(tmpdir.path);
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
watcher.watchPath(tmpdir.resolve('subdirectory'));
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
});
it('should remove existing watcher if adding a parent directory watcher',
{ skip: !supportsRecursiveWatching }, () => {
assert.deepStrictEqual(watcher.watchedPaths, []);
const subdirectory = tmpdir.resolve('subdirectory');
mkdirSync(subdirectory);
watcher.watchPath(subdirectory);
assert.deepStrictEqual(watcher.watchedPaths, [subdirectory]);
watcher.watchPath(tmpdir.path);
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
});
it('should clear all watchers when calling clear',
{ skip: !supportsRecursiveWatching }, () => {
assert.deepStrictEqual(watcher.watchedPaths, []);
watcher.watchPath(tmpdir.path);
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
watcher.clear();
assert.deepStrictEqual(watcher.watchedPaths, []);
});
it('should watch files from subprocess IPC events', async () => {
const file = fixtures.path('watch-mode/ipc.js');
const child = spawn(process.execPath, [file], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], encoding: 'utf8' });
watcher.watchChildProcessModules(child);
await once(child, 'exit');
let expected = [file, tmpdir.resolve('file')];
if (supportsRecursiveWatching) {
expected = expected.map((file) => path.dirname(file));
}
assert.deepStrictEqual(watcher.watchedPaths, expected);
});
});