Path: blob/main/src/vs/platform/files/test/node/parcelWatcher.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { realpathSync, promises } from 'fs';7import { tmpdir } from 'os';8import { timeout } from '../../../../base/common/async.js';9import { dirname, join } from '../../../../base/common/path.js';10import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';11import { Promises, RimRafMode } from '../../../../base/node/pfs.js';12import { getRandomTestPath } from '../../../../base/test/node/testUtils.js';13import { FileChangeFilter, FileChangeType, IFileChange } from '../../common/files.js';14import { ParcelWatcher } from '../../node/watcher/parcel/parcelWatcher.js';15import { IRecursiveWatchRequest } from '../../common/watcher.js';16import { getDriveLetter } from '../../../../base/common/extpath.js';17import { ltrim } from '../../../../base/common/strings.js';18import { FileAccess } from '../../../../base/common/network.js';19import { extUriBiasedIgnorePathCase } from '../../../../base/common/resources.js';20import { URI } from '../../../../base/common/uri.js';21import { addUNCHostToAllowlist } from '../../../../base/node/unc.js';22import { Emitter, Event } from '../../../../base/common/event.js';23import { DisposableStore } from '../../../../base/common/lifecycle.js';2425export class TestParcelWatcher extends ParcelWatcher {2627protected override readonly suspendedWatchRequestPollingInterval = 100;2829private readonly _onDidWatch = this._register(new Emitter<void>());30readonly onDidWatch = this._onDidWatch.event;3132readonly onWatchFail = this._onDidWatchFail.event;3334async testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): Promise<string[]> {3536// Work with strings as paths to simplify testing37const requests: IRecursiveWatchRequest[] = paths.map(path => {38return { path, excludes, recursive: true };39});4041return (await this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */)).map(request => request.path);42}4344protected override getUpdateWatchersDelay(): number {45return 0;46}4748protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {49await super.doWatch(requests);50await this.whenReady();5152this._onDidWatch.fire();53}5455async whenReady(): Promise<void> {56for (const watcher of this.watchers) {57await watcher.ready;58}59}60}6162// this suite has shown flaky runs in Azure pipelines where63// tasks would just hang and timeout after a while (not in64// mocha but generally). as such they will run only on demand65// whenever we update the watcher library.6667suite.skip('File Watcher (parcel)', function () {6869this.timeout(10000);7071let testDir: string;72let watcher: TestParcelWatcher;7374let loggingEnabled = false;7576function enableLogging(enable: boolean) {77loggingEnabled = enable;78watcher?.setVerboseLogging(enable);79}8081enableLogging(loggingEnabled);8283setup(async () => {84watcher = new TestParcelWatcher();85watcher.setVerboseLogging(loggingEnabled);8687watcher.onDidLogMessage(e => {88if (loggingEnabled) {89console.log(`[recursive watcher test message] ${e.message}`);90}91});9293watcher.onDidError(e => {94if (loggingEnabled) {95console.log(`[recursive watcher test error] ${e.error}`);96}97});9899// Rule out strange testing conditions by using the realpath100// here. for example, on macOS the tmp dir is potentially a101// symlink in some of the root folders, which is a rather102// unrealisic case for the file watcher.103testDir = URI.file(getRandomTestPath(realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath;104105const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;106107await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });108});109110teardown(async () => {111const watchers = Array.from(watcher.watchers).length;112let stoppedInstances = 0;113for (const instance of watcher.watchers) {114Event.once(instance.onDidStop)(() => {115if (instance.stopped) {116stoppedInstances++;117}118});119}120121await watcher.stop();122assert.strictEqual(stoppedInstances, watchers, 'All watchers must be stopped before the test ends');123watcher.dispose();124125// Possible that the file watcher is still holding126// onto the folders on Windows specifically and the127// unlink would fail. In that case, do not fail the128// test suite.129return Promises.rm(testDir).catch(error => console.error(error));130});131132function toMsg(type: FileChangeType): string {133switch (type) {134case FileChangeType.ADDED: return 'added';135case FileChangeType.DELETED: return 'deleted';136default: return 'changed';137}138}139140async function awaitEvent(watcher: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string, correlationId?: number | null, expectedCount?: number): Promise<IFileChange[]> {141if (loggingEnabled) {142console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);143}144145// Await the event146const res = await new Promise<IFileChange[]>((resolve, reject) => {147let counter = 0;148const disposable = watcher.onDidChangeFile(events => {149for (const event of events) {150if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {151counter++;152if (typeof expectedCount === 'number' && counter < expectedCount) {153continue; // not yet154}155156disposable.dispose();157if (failOnEventReason) {158reject(new Error(`Unexpected file event: ${failOnEventReason}`));159} else {160setImmediate(() => resolve(events)); // copied from parcel watcher tests, seems to drop unrelated events on macOS161}162break;163}164}165});166});167168// Unwind from the event call stack: we have seen crashes in Parcel169// when e.g. calling `unsubscribe` directly from the stack of a file170// change event171// Refs: https://github.com/microsoft/vscode/issues/137430172await timeout(1);173174return res;175}176177function awaitMessage(watcher: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {178if (loggingEnabled) {179console.log(`Awaiting message of type ${type}`);180}181182// Await the message183return new Promise<void>(resolve => {184const disposable = watcher.onDidLogMessage(msg => {185if (msg.type === type) {186disposable.dispose();187resolve();188}189});190});191}192193test('basics', async function () {194const request = { path: testDir, excludes: [], recursive: true };195await watcher.watch([request]);196197const instance = Array.from(watcher.watchers)[0];198assert.strictEqual(request, instance.request);199assert.strictEqual(instance.failed, false);200assert.strictEqual(instance.stopped, false);201202const disposables = new DisposableStore();203204const subscriptions1 = new Map<string, FileChangeType>();205const subscriptions2 = new Map<string, FileChangeType>();206207// New file208const newFilePath = join(testDir, 'deep', 'newFile.txt');209disposables.add(instance.subscribe(newFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));210disposables.add(instance.subscribe(newFilePath, change => subscriptions2.set(change.resource.fsPath, change.type))); // can subscribe multiple times211assert.strictEqual(instance.include(newFilePath), true);212assert.strictEqual(instance.exclude(newFilePath), false);213let changeFuture: Promise<unknown> = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);214await Promises.writeFile(newFilePath, 'Hello World');215await changeFuture;216assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.ADDED);217assert.strictEqual(subscriptions2.get(newFilePath), FileChangeType.ADDED);218219// New folder220const newFolderPath = join(testDir, 'deep', 'New Folder');221disposables.add(instance.subscribe(newFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));222const disposable = instance.subscribe(newFolderPath, change => subscriptions2.set(change.resource.fsPath, change.type));223disposable.dispose();224assert.strictEqual(instance.include(newFolderPath), true);225assert.strictEqual(instance.exclude(newFolderPath), false);226changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED);227await promises.mkdir(newFolderPath);228await changeFuture;229assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.ADDED);230assert.strictEqual(subscriptions2.has(newFolderPath), false /* subscription was disposed before the event */);231232// Rename file233let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt');234disposables.add(instance.subscribe(renamedFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));235changeFuture = Promise.all([236awaitEvent(watcher, newFilePath, FileChangeType.DELETED),237awaitEvent(watcher, renamedFilePath, FileChangeType.ADDED)238]);239await Promises.rename(newFilePath, renamedFilePath);240await changeFuture;241assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.DELETED);242assert.strictEqual(subscriptions1.get(renamedFilePath), FileChangeType.ADDED);243244// Rename folder245let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder');246disposables.add(instance.subscribe(renamedFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));247changeFuture = Promise.all([248awaitEvent(watcher, newFolderPath, FileChangeType.DELETED),249awaitEvent(watcher, renamedFolderPath, FileChangeType.ADDED)250]);251await Promises.rename(newFolderPath, renamedFolderPath);252await changeFuture;253assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.DELETED);254assert.strictEqual(subscriptions1.get(renamedFolderPath), FileChangeType.ADDED);255256// Rename file (same name, different case)257const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');258changeFuture = Promise.all([259awaitEvent(watcher, renamedFilePath, FileChangeType.DELETED),260awaitEvent(watcher, caseRenamedFilePath, FileChangeType.ADDED)261]);262await Promises.rename(renamedFilePath, caseRenamedFilePath);263await changeFuture;264renamedFilePath = caseRenamedFilePath;265266// Rename folder (same name, different case)267const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder');268changeFuture = Promise.all([269awaitEvent(watcher, renamedFolderPath, FileChangeType.DELETED),270awaitEvent(watcher, caseRenamedFolderPath, FileChangeType.ADDED)271]);272await Promises.rename(renamedFolderPath, caseRenamedFolderPath);273await changeFuture;274renamedFolderPath = caseRenamedFolderPath;275276// Move file277const movedFilepath = join(testDir, 'movedFile.txt');278changeFuture = Promise.all([279awaitEvent(watcher, renamedFilePath, FileChangeType.DELETED),280awaitEvent(watcher, movedFilepath, FileChangeType.ADDED)281]);282await Promises.rename(renamedFilePath, movedFilepath);283await changeFuture;284285// Move folder286const movedFolderpath = join(testDir, 'Moved Folder');287changeFuture = Promise.all([288awaitEvent(watcher, renamedFolderPath, FileChangeType.DELETED),289awaitEvent(watcher, movedFolderpath, FileChangeType.ADDED)290]);291await Promises.rename(renamedFolderPath, movedFolderpath);292await changeFuture;293294// Copy file295const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt');296changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED);297await promises.copyFile(movedFilepath, copiedFilepath);298await changeFuture;299300// Copy folder301const copiedFolderpath = join(testDir, 'deep', 'Copied Folder');302changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.ADDED);303await Promises.copy(movedFolderpath, copiedFolderpath, { preserveSymlinks: false });304await changeFuture;305306// Change file307changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.UPDATED);308await Promises.writeFile(copiedFilepath, 'Hello Change');309await changeFuture;310311// Create new file312const anotherNewFilePath = join(testDir, 'deep', 'anotherNewFile.txt');313changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.ADDED);314await Promises.writeFile(anotherNewFilePath, 'Hello Another World');315await changeFuture;316317// Read file does not emit event318changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-read-file');319await promises.readFile(anotherNewFilePath);320await Promise.race([timeout(100), changeFuture]);321322// Stat file does not emit event323changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-stat');324await promises.stat(anotherNewFilePath);325await Promise.race([timeout(100), changeFuture]);326327// Stat folder does not emit event328changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.UPDATED, 'unexpected-event-from-stat');329await promises.stat(copiedFolderpath);330await Promise.race([timeout(100), changeFuture]);331332// Delete file333changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED);334disposables.add(instance.subscribe(copiedFilepath, change => subscriptions1.set(change.resource.fsPath, change.type)));335await promises.unlink(copiedFilepath);336await changeFuture;337assert.strictEqual(subscriptions1.get(copiedFilepath), FileChangeType.DELETED);338339// Delete folder340changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED);341disposables.add(instance.subscribe(copiedFolderpath, change => subscriptions1.set(change.resource.fsPath, change.type)));342await promises.rmdir(copiedFolderpath);343await changeFuture;344assert.strictEqual(subscriptions1.get(copiedFolderpath), FileChangeType.DELETED);345346disposables.dispose();347});348349(isMacintosh /* this test seems not possible with fsevents backend */ ? test.skip : test)('basics (atomic writes)', async function () {350await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);351352// Delete + Recreate file353const newFilePath = join(testDir, 'deep', 'conway.js');354const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED);355await promises.unlink(newFilePath);356Promises.writeFile(newFilePath, 'Hello Atomic World');357await changeFuture;358});359360(!isLinux /* polling is only used in linux environments (WSL) */ ? test.skip : test)('basics (polling)', async function () {361await watcher.watch([{ path: testDir, excludes: [], pollingInterval: 100, recursive: true }]);362363return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));364});365366async function basicCrudTest(filePath: string, correlationId?: number | null, expectedCount?: number): Promise<void> {367368// New file369let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, correlationId, expectedCount);370await Promises.writeFile(filePath, 'Hello World');371await changeFuture;372373// Change file374changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, undefined, correlationId, expectedCount);375await Promises.writeFile(filePath, 'Hello Change');376await changeFuture;377378// Delete file379changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount);380await promises.unlink(filePath);381await changeFuture;382}383384test('multiple events', async function () {385await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);386await promises.mkdir(join(testDir, 'deep-multiple'));387388// multiple add389390const newFilePath1 = join(testDir, 'newFile-1.txt');391const newFilePath2 = join(testDir, 'newFile-2.txt');392const newFilePath3 = join(testDir, 'newFile-3.txt');393const newFilePath4 = join(testDir, 'deep-multiple', 'newFile-1.txt');394const newFilePath5 = join(testDir, 'deep-multiple', 'newFile-2.txt');395const newFilePath6 = join(testDir, 'deep-multiple', 'newFile-3.txt');396397const addedFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.ADDED);398const addedFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.ADDED);399const addedFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.ADDED);400const addedFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.ADDED);401const addedFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.ADDED);402const addedFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.ADDED);403404await Promise.all([405await Promises.writeFile(newFilePath1, 'Hello World 1'),406await Promises.writeFile(newFilePath2, 'Hello World 2'),407await Promises.writeFile(newFilePath3, 'Hello World 3'),408await Promises.writeFile(newFilePath4, 'Hello World 4'),409await Promises.writeFile(newFilePath5, 'Hello World 5'),410await Promises.writeFile(newFilePath6, 'Hello World 6')411]);412413await Promise.all([addedFuture1, addedFuture2, addedFuture3, addedFuture4, addedFuture5, addedFuture6]);414415// multiple change416417const changeFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.UPDATED);418const changeFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.UPDATED);419const changeFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.UPDATED);420const changeFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.UPDATED);421const changeFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.UPDATED);422const changeFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.UPDATED);423424await Promise.all([425await Promises.writeFile(newFilePath1, 'Hello Update 1'),426await Promises.writeFile(newFilePath2, 'Hello Update 2'),427await Promises.writeFile(newFilePath3, 'Hello Update 3'),428await Promises.writeFile(newFilePath4, 'Hello Update 4'),429await Promises.writeFile(newFilePath5, 'Hello Update 5'),430await Promises.writeFile(newFilePath6, 'Hello Update 6')431]);432433await Promise.all([changeFuture1, changeFuture2, changeFuture3, changeFuture4, changeFuture5, changeFuture6]);434435// copy with multiple files436437const copyFuture1 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-1.txt'), FileChangeType.ADDED);438const copyFuture2 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-2.txt'), FileChangeType.ADDED);439const copyFuture3 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-3.txt'), FileChangeType.ADDED);440const copyFuture4 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy'), FileChangeType.ADDED);441442await Promises.copy(join(testDir, 'deep-multiple'), join(testDir, 'deep-multiple-copy'), { preserveSymlinks: false });443444await Promise.all([copyFuture1, copyFuture2, copyFuture3, copyFuture4]);445446// multiple delete (single files)447448const deleteFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.DELETED);449const deleteFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.DELETED);450const deleteFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.DELETED);451const deleteFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.DELETED);452const deleteFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.DELETED);453const deleteFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.DELETED);454455await Promise.all([456await promises.unlink(newFilePath1),457await promises.unlink(newFilePath2),458await promises.unlink(newFilePath3),459await promises.unlink(newFilePath4),460await promises.unlink(newFilePath5),461await promises.unlink(newFilePath6)462]);463464await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]);465466// multiple delete (folder)467468const deleteFolderFuture1 = awaitEvent(watcher, join(testDir, 'deep-multiple'), FileChangeType.DELETED);469const deleteFolderFuture2 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy'), FileChangeType.DELETED);470471await Promise.all([Promises.rm(join(testDir, 'deep-multiple'), RimRafMode.UNLINK), Promises.rm(join(testDir, 'deep-multiple-copy'), RimRafMode.UNLINK)]);472473await Promise.all([deleteFolderFuture1, deleteFolderFuture2]);474});475476test('subsequent watch updates watchers (path)', async function () {477await watcher.watch([{ path: testDir, excludes: [join(realpathSync(testDir), 'unrelated')], recursive: true }]);478479// New file (*.txt)480let newTextFilePath = join(testDir, 'deep', 'newFile.txt');481let changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);482await Promises.writeFile(newTextFilePath, 'Hello World');483await changeFuture;484485await watcher.watch([{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'unrelated')], recursive: true }]);486newTextFilePath = join(testDir, 'deep', 'newFile2.txt');487changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);488await Promises.writeFile(newTextFilePath, 'Hello World');489await changeFuture;490491await watcher.watch([{ path: join(testDir, 'deep'), excludes: [realpathSync(testDir)], recursive: true }]);492await watcher.watch([{ path: join(testDir, 'deep'), excludes: [], recursive: true }]);493newTextFilePath = join(testDir, 'deep', 'newFile3.txt');494changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);495await Promises.writeFile(newTextFilePath, 'Hello World');496await changeFuture;497});498499test('invalid path does not crash watcher', async function () {500await watcher.watch([501{ path: testDir, excludes: [], recursive: true },502{ path: join(testDir, 'invalid-folder'), excludes: [], recursive: true },503{ path: FileAccess.asFileUri('').fsPath, excludes: [], recursive: true }504]);505506return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));507});508509test('subsequent watch updates watchers (excludes)', async function () {510await watcher.watch([{ path: testDir, excludes: [realpathSync(testDir)], recursive: true }]);511await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);512513return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));514});515516test('subsequent watch updates watchers (includes)', async function () {517await watcher.watch([{ path: testDir, excludes: [], includes: ['nothing'], recursive: true }]);518await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);519520return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));521});522523test('includes are supported', async function () {524await watcher.watch([{ path: testDir, excludes: [], includes: ['**/deep/**'], recursive: true }]);525526return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));527});528529test('includes are supported (relative pattern explicit)', async function () {530await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'deep/newFile.txt' }], recursive: true }]);531532return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));533});534535test('includes are supported (relative pattern implicit)', async function () {536await watcher.watch([{ path: testDir, excludes: [], includes: ['deep/newFile.txt'], recursive: true }]);537538return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));539});540541test('excludes are supported (path)', async function () {542return testExcludes([join(realpathSync(testDir), 'deep')]);543});544545test('excludes are supported (glob)', function () {546return testExcludes(['deep/**']);547});548549async function testExcludes(excludes: string[]) {550await watcher.watch([{ path: testDir, excludes, recursive: true }]);551552// New file (*.txt)553const newTextFilePath = join(testDir, 'deep', 'newFile.txt');554const changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);555await Promises.writeFile(newTextFilePath, 'Hello World');556557const res = await Promise.any([558timeout(500).then(() => true),559changeFuture.then(() => false)560]);561562if (!res) {563assert.fail('Unexpected change event');564}565}566567(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () {568const link = join(testDir, 'deep-linked');569const linkTarget = join(testDir, 'deep');570await promises.symlink(linkTarget, link);571572await watcher.watch([{ path: link, excludes: [], recursive: true }]);573574return basicCrudTest(join(link, 'newFile.txt'));575});576577(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () {578const link = join(testDir, 'deep-linked');579const linkTarget = join(testDir, 'deep');580await promises.symlink(linkTarget, link);581582await watcher.watch([{ path: testDir, excludes: [], recursive: true }, { path: link, excludes: [], recursive: true }]);583584return basicCrudTest(join(link, 'newFile.txt'));585});586587(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support', async function () {588addUNCHostToAllowlist('localhost');589590// Local UNC paths are in the form of: \\localhost\c$\my_dir591const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;592593await watcher.watch([{ path: uncPath, excludes: [], recursive: true }]);594595return basicCrudTest(join(uncPath, 'deep', 'newFile.txt'));596});597598(isLinux /* linux: is case sensitive */ ? test.skip : test)('wrong casing', async function () {599const deepWrongCasedPath = join(testDir, 'DEEP');600601await watcher.watch([{ path: deepWrongCasedPath, excludes: [], recursive: true }]);602603return basicCrudTest(join(deepWrongCasedPath, 'newFile.txt'));604});605606test('invalid folder does not explode', async function () {607const invalidPath = join(testDir, 'invalid');608609await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]);610});611612(isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path without correlation restarts watching', async function () {613const watchedPath = join(testDir, 'deep');614615await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]);616617// Delete watched path and await618const warnFuture = awaitMessage(watcher, 'warn');619await Promises.rm(watchedPath, RimRafMode.UNLINK);620await warnFuture;621622// Restore watched path623await timeout(1500); // node.js watcher used for monitoring folder restore is async624await promises.mkdir(watchedPath);625await timeout(1500); // restart is delayed626await watcher.whenReady();627628// Verify events come in again629const newFilePath = join(watchedPath, 'newFile.txt');630const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);631await Promises.writeFile(newFilePath, 'Hello World');632await changeFuture;633});634635test('correlationId is supported', async function () {636const correlationId = Math.random();637await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: true }]);638639return basicCrudTest(join(testDir, 'newFile.txt'), correlationId);640});641642test('should not exclude roots that do not overlap', async () => {643if (isWindows) {644assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']);645assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);646assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);647} else {648assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a']), ['/a']);649assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']);650assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);651}652});653654test('should remove sub-folders of other paths', async () => {655if (isWindows) {656assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']);657assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);658assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);659assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);660} else {661assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']);662assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']);663assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);664assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']);665}666});667668test('should ignore when everything excluded', async () => {669assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []);670});671672test('watching same or overlapping paths supported when correlation is applied', async () => {673await watcher.watch([674{ path: testDir, excludes: [], recursive: true, correlationId: 1 }675]);676677await basicCrudTest(join(testDir, 'newFile.txt'), null, 1);678679// same path, same options680await watcher.watch([681{ path: testDir, excludes: [], recursive: true, correlationId: 1 },682{ path: testDir, excludes: [], recursive: true, correlationId: 2, },683{ path: testDir, excludes: [], recursive: true, correlationId: undefined }684]);685686await basicCrudTest(join(testDir, 'newFile.txt'), null, 3);687await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 3);688689// same path, different options690await watcher.watch([691{ path: testDir, excludes: [], recursive: true, correlationId: 1 },692{ path: testDir, excludes: [], recursive: true, correlationId: 2 },693{ path: testDir, excludes: [], recursive: true, correlationId: undefined },694{ path: testDir, excludes: [join(realpathSync(testDir), 'deep')], recursive: true, correlationId: 3 },695{ path: testDir, excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 4 },696]);697698await basicCrudTest(join(testDir, 'newFile.txt'), null, 5);699await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 5);700701// overlapping paths (same options)702await watcher.watch([703{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },704{ path: testDir, excludes: [], recursive: true, correlationId: 2 },705{ path: join(testDir, 'deep'), excludes: [], recursive: true, correlationId: 3 },706]);707708await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);709await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);710711// overlapping paths (different options)712await watcher.watch([713{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },714{ path: testDir, excludes: [join(realpathSync(testDir), 'some')], recursive: true, correlationId: 2 },715{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 3 },716]);717718await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);719await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);720});721722test('watching missing path emits watcher fail event', async function () {723const onDidWatchFail = Event.toPromise(watcher.onWatchFail);724725const folderPath = join(testDir, 'missing');726watcher.watch([{ path: folderPath, excludes: [], recursive: true }]);727728await onDidWatchFail;729});730731test('deleting watched path emits watcher fail and delete event if correlated', async function () {732const folderPath = join(testDir, 'deep');733734await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);735736let failed = false;737const instance = Array.from(watcher.watchers)[0];738assert.strictEqual(instance.include(folderPath), true);739instance.onDidFail(() => failed = true);740741const onDidWatchFail = Event.toPromise(watcher.onWatchFail);742const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1);743Promises.rm(folderPath, RimRafMode.UNLINK);744await onDidWatchFail;745await changeFuture;746assert.strictEqual(failed, true);747assert.strictEqual(instance.failed, true);748});749750(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => {751await testWatchFolderDoesNotExist(false);752});753754test('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => {755await testWatchFolderDoesNotExist(true);756});757758async function testWatchFolderDoesNotExist(reuseExistingWatcher: boolean) {759let onDidWatchFail = Event.toPromise(watcher.onWatchFail);760761const folderPath = join(testDir, 'not-found');762763const requests: IRecursiveWatchRequest[] = [];764if (reuseExistingWatcher) {765requests.push({ path: testDir, excludes: [], recursive: true });766await watcher.watch(requests);767}768769const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true };770requests.push(request);771772await watcher.watch(requests);773await onDidWatchFail;774775if (reuseExistingWatcher) {776assert.strictEqual(watcher.isSuspended(request), true);777} else {778assert.strictEqual(watcher.isSuspended(request), 'polling');779}780781let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);782let onDidWatch = Event.toPromise(watcher.onDidWatch);783await promises.mkdir(folderPath);784await changeFuture;785await onDidWatch;786787assert.strictEqual(watcher.isSuspended(request), false);788789const filePath = join(folderPath, 'newFile.txt');790await basicCrudTest(filePath);791792if (!reuseExistingWatcher) {793onDidWatchFail = Event.toPromise(watcher.onWatchFail);794await Promises.rm(folderPath);795await onDidWatchFail;796797changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);798onDidWatch = Event.toPromise(watcher.onDidWatch);799await promises.mkdir(folderPath);800await changeFuture;801await onDidWatch;802803await basicCrudTest(filePath);804}805}806807(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => {808await testWatchFolderExists(false);809});810811test('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => {812await testWatchFolderExists(true);813});814815async function testWatchFolderExists(reuseExistingWatcher: boolean) {816const folderPath = join(testDir, 'deep');817818const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true }];819if (reuseExistingWatcher) {820requests.push({ path: testDir, excludes: [], recursive: true });821}822823await watcher.watch(requests);824825const filePath = join(folderPath, 'newFile.txt');826await basicCrudTest(filePath);827828if (!reuseExistingWatcher) {829const onDidWatchFail = Event.toPromise(watcher.onWatchFail);830await Promises.rm(folderPath);831await onDidWatchFail;832833const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);834const onDidWatch = Event.toPromise(watcher.onDidWatch);835await promises.mkdir(folderPath);836await changeFuture;837await onDidWatch;838839await basicCrudTest(filePath);840}841}842843test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () {844const folderPath1 = join(testDir, 'deep', 'not-existing1');845const folderPath2 = join(testDir, 'deep', 'not-existing2');846const folderPath3 = join(testDir, 'not-existing3');847848const requests: IRecursiveWatchRequest[] = [849{ path: folderPath1, excludes: [], recursive: true, correlationId: 1 },850{ path: folderPath2, excludes: [], recursive: true, correlationId: 2 },851{ path: folderPath3, excludes: [], recursive: true, correlationId: 3 },852{ path: join(testDir, 'deep'), excludes: [], recursive: true }853];854855await watcher.watch(requests);856857assert.strictEqual(watcher.isSuspended(requests[0]), true);858assert.strictEqual(watcher.isSuspended(requests[1]), true);859assert.strictEqual(watcher.isSuspended(requests[2]), 'polling');860assert.strictEqual(watcher.isSuspended(requests[3]), false);861});862863test('event type filter', async function () {864const request = { path: testDir, excludes: [], recursive: true, filter: FileChangeFilter.ADDED | FileChangeFilter.DELETED, correlationId: 1 };865await watcher.watch([request]);866867// Change file868const filePath = join(testDir, 'lorem-newfile.txt');869let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, 1);870await Promises.writeFile(filePath, 'Hello Change');871await changeFuture;872873// Delete file874changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, 1);875await promises.unlink(filePath);876await changeFuture;877});878});879880881