Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/test/node/parcelWatcher.test.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { realpathSync, promises } from 'fs';
8
import { tmpdir } from 'os';
9
import { timeout } from '../../../../base/common/async.js';
10
import { dirname, join } from '../../../../base/common/path.js';
11
import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
12
import { Promises, RimRafMode } from '../../../../base/node/pfs.js';
13
import { getRandomTestPath } from '../../../../base/test/node/testUtils.js';
14
import { FileChangeFilter, FileChangeType, IFileChange } from '../../common/files.js';
15
import { ParcelWatcher } from '../../node/watcher/parcel/parcelWatcher.js';
16
import { IRecursiveWatchRequest } from '../../common/watcher.js';
17
import { getDriveLetter } from '../../../../base/common/extpath.js';
18
import { ltrim } from '../../../../base/common/strings.js';
19
import { FileAccess } from '../../../../base/common/network.js';
20
import { extUriBiasedIgnorePathCase } from '../../../../base/common/resources.js';
21
import { URI } from '../../../../base/common/uri.js';
22
import { addUNCHostToAllowlist } from '../../../../base/node/unc.js';
23
import { Emitter, Event } from '../../../../base/common/event.js';
24
import { DisposableStore } from '../../../../base/common/lifecycle.js';
25
26
export class TestParcelWatcher extends ParcelWatcher {
27
28
protected override readonly suspendedWatchRequestPollingInterval = 100;
29
30
private readonly _onDidWatch = this._register(new Emitter<void>());
31
readonly onDidWatch = this._onDidWatch.event;
32
33
readonly onWatchFail = this._onDidWatchFail.event;
34
35
async testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): Promise<string[]> {
36
37
// Work with strings as paths to simplify testing
38
const requests: IRecursiveWatchRequest[] = paths.map(path => {
39
return { path, excludes, recursive: true };
40
});
41
42
return (await this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */)).map(request => request.path);
43
}
44
45
protected override getUpdateWatchersDelay(): number {
46
return 0;
47
}
48
49
protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {
50
await super.doWatch(requests);
51
await this.whenReady();
52
53
this._onDidWatch.fire();
54
}
55
56
async whenReady(): Promise<void> {
57
for (const watcher of this.watchers) {
58
await watcher.ready;
59
}
60
}
61
}
62
63
// this suite has shown flaky runs in Azure pipelines where
64
// tasks would just hang and timeout after a while (not in
65
// mocha but generally). as such they will run only on demand
66
// whenever we update the watcher library.
67
68
suite.skip('File Watcher (parcel)', function () {
69
70
this.timeout(10000);
71
72
let testDir: string;
73
let watcher: TestParcelWatcher;
74
75
let loggingEnabled = false;
76
77
function enableLogging(enable: boolean) {
78
loggingEnabled = enable;
79
watcher?.setVerboseLogging(enable);
80
}
81
82
enableLogging(loggingEnabled);
83
84
setup(async () => {
85
watcher = new TestParcelWatcher();
86
watcher.setVerboseLogging(loggingEnabled);
87
88
watcher.onDidLogMessage(e => {
89
if (loggingEnabled) {
90
console.log(`[recursive watcher test message] ${e.message}`);
91
}
92
});
93
94
watcher.onDidError(e => {
95
if (loggingEnabled) {
96
console.log(`[recursive watcher test error] ${e.error}`);
97
}
98
});
99
100
// Rule out strange testing conditions by using the realpath
101
// here. for example, on macOS the tmp dir is potentially a
102
// symlink in some of the root folders, which is a rather
103
// unrealisic case for the file watcher.
104
testDir = URI.file(getRandomTestPath(realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath;
105
106
const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;
107
108
await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });
109
});
110
111
teardown(async () => {
112
const watchers = Array.from(watcher.watchers).length;
113
let stoppedInstances = 0;
114
for (const instance of watcher.watchers) {
115
Event.once(instance.onDidStop)(() => {
116
if (instance.stopped) {
117
stoppedInstances++;
118
}
119
});
120
}
121
122
await watcher.stop();
123
assert.strictEqual(stoppedInstances, watchers, 'All watchers must be stopped before the test ends');
124
watcher.dispose();
125
126
// Possible that the file watcher is still holding
127
// onto the folders on Windows specifically and the
128
// unlink would fail. In that case, do not fail the
129
// test suite.
130
return Promises.rm(testDir).catch(error => console.error(error));
131
});
132
133
function toMsg(type: FileChangeType): string {
134
switch (type) {
135
case FileChangeType.ADDED: return 'added';
136
case FileChangeType.DELETED: return 'deleted';
137
default: return 'changed';
138
}
139
}
140
141
async function awaitEvent(watcher: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string, correlationId?: number | null, expectedCount?: number): Promise<IFileChange[]> {
142
if (loggingEnabled) {
143
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
144
}
145
146
// Await the event
147
const res = await new Promise<IFileChange[]>((resolve, reject) => {
148
let counter = 0;
149
const disposable = watcher.onDidChangeFile(events => {
150
for (const event of events) {
151
if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {
152
counter++;
153
if (typeof expectedCount === 'number' && counter < expectedCount) {
154
continue; // not yet
155
}
156
157
disposable.dispose();
158
if (failOnEventReason) {
159
reject(new Error(`Unexpected file event: ${failOnEventReason}`));
160
} else {
161
setImmediate(() => resolve(events)); // copied from parcel watcher tests, seems to drop unrelated events on macOS
162
}
163
break;
164
}
165
}
166
});
167
});
168
169
// Unwind from the event call stack: we have seen crashes in Parcel
170
// when e.g. calling `unsubscribe` directly from the stack of a file
171
// change event
172
// Refs: https://github.com/microsoft/vscode/issues/137430
173
await timeout(1);
174
175
return res;
176
}
177
178
function awaitMessage(watcher: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {
179
if (loggingEnabled) {
180
console.log(`Awaiting message of type ${type}`);
181
}
182
183
// Await the message
184
return new Promise<void>(resolve => {
185
const disposable = watcher.onDidLogMessage(msg => {
186
if (msg.type === type) {
187
disposable.dispose();
188
resolve();
189
}
190
});
191
});
192
}
193
194
test('basics', async function () {
195
const request = { path: testDir, excludes: [], recursive: true };
196
await watcher.watch([request]);
197
198
const instance = Array.from(watcher.watchers)[0];
199
assert.strictEqual(request, instance.request);
200
assert.strictEqual(instance.failed, false);
201
assert.strictEqual(instance.stopped, false);
202
203
const disposables = new DisposableStore();
204
205
const subscriptions1 = new Map<string, FileChangeType>();
206
const subscriptions2 = new Map<string, FileChangeType>();
207
208
// New file
209
const newFilePath = join(testDir, 'deep', 'newFile.txt');
210
disposables.add(instance.subscribe(newFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));
211
disposables.add(instance.subscribe(newFilePath, change => subscriptions2.set(change.resource.fsPath, change.type))); // can subscribe multiple times
212
assert.strictEqual(instance.include(newFilePath), true);
213
assert.strictEqual(instance.exclude(newFilePath), false);
214
let changeFuture: Promise<unknown> = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);
215
await Promises.writeFile(newFilePath, 'Hello World');
216
await changeFuture;
217
assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.ADDED);
218
assert.strictEqual(subscriptions2.get(newFilePath), FileChangeType.ADDED);
219
220
// New folder
221
const newFolderPath = join(testDir, 'deep', 'New Folder');
222
disposables.add(instance.subscribe(newFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));
223
const disposable = instance.subscribe(newFolderPath, change => subscriptions2.set(change.resource.fsPath, change.type));
224
disposable.dispose();
225
assert.strictEqual(instance.include(newFolderPath), true);
226
assert.strictEqual(instance.exclude(newFolderPath), false);
227
changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED);
228
await promises.mkdir(newFolderPath);
229
await changeFuture;
230
assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.ADDED);
231
assert.strictEqual(subscriptions2.has(newFolderPath), false /* subscription was disposed before the event */);
232
233
// Rename file
234
let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt');
235
disposables.add(instance.subscribe(renamedFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));
236
changeFuture = Promise.all([
237
awaitEvent(watcher, newFilePath, FileChangeType.DELETED),
238
awaitEvent(watcher, renamedFilePath, FileChangeType.ADDED)
239
]);
240
await Promises.rename(newFilePath, renamedFilePath);
241
await changeFuture;
242
assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.DELETED);
243
assert.strictEqual(subscriptions1.get(renamedFilePath), FileChangeType.ADDED);
244
245
// Rename folder
246
let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder');
247
disposables.add(instance.subscribe(renamedFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));
248
changeFuture = Promise.all([
249
awaitEvent(watcher, newFolderPath, FileChangeType.DELETED),
250
awaitEvent(watcher, renamedFolderPath, FileChangeType.ADDED)
251
]);
252
await Promises.rename(newFolderPath, renamedFolderPath);
253
await changeFuture;
254
assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.DELETED);
255
assert.strictEqual(subscriptions1.get(renamedFolderPath), FileChangeType.ADDED);
256
257
// Rename file (same name, different case)
258
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
259
changeFuture = Promise.all([
260
awaitEvent(watcher, renamedFilePath, FileChangeType.DELETED),
261
awaitEvent(watcher, caseRenamedFilePath, FileChangeType.ADDED)
262
]);
263
await Promises.rename(renamedFilePath, caseRenamedFilePath);
264
await changeFuture;
265
renamedFilePath = caseRenamedFilePath;
266
267
// Rename folder (same name, different case)
268
const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder');
269
changeFuture = Promise.all([
270
awaitEvent(watcher, renamedFolderPath, FileChangeType.DELETED),
271
awaitEvent(watcher, caseRenamedFolderPath, FileChangeType.ADDED)
272
]);
273
await Promises.rename(renamedFolderPath, caseRenamedFolderPath);
274
await changeFuture;
275
renamedFolderPath = caseRenamedFolderPath;
276
277
// Move file
278
const movedFilepath = join(testDir, 'movedFile.txt');
279
changeFuture = Promise.all([
280
awaitEvent(watcher, renamedFilePath, FileChangeType.DELETED),
281
awaitEvent(watcher, movedFilepath, FileChangeType.ADDED)
282
]);
283
await Promises.rename(renamedFilePath, movedFilepath);
284
await changeFuture;
285
286
// Move folder
287
const movedFolderpath = join(testDir, 'Moved Folder');
288
changeFuture = Promise.all([
289
awaitEvent(watcher, renamedFolderPath, FileChangeType.DELETED),
290
awaitEvent(watcher, movedFolderpath, FileChangeType.ADDED)
291
]);
292
await Promises.rename(renamedFolderPath, movedFolderpath);
293
await changeFuture;
294
295
// Copy file
296
const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt');
297
changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED);
298
await promises.copyFile(movedFilepath, copiedFilepath);
299
await changeFuture;
300
301
// Copy folder
302
const copiedFolderpath = join(testDir, 'deep', 'Copied Folder');
303
changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.ADDED);
304
await Promises.copy(movedFolderpath, copiedFolderpath, { preserveSymlinks: false });
305
await changeFuture;
306
307
// Change file
308
changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.UPDATED);
309
await Promises.writeFile(copiedFilepath, 'Hello Change');
310
await changeFuture;
311
312
// Create new file
313
const anotherNewFilePath = join(testDir, 'deep', 'anotherNewFile.txt');
314
changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.ADDED);
315
await Promises.writeFile(anotherNewFilePath, 'Hello Another World');
316
await changeFuture;
317
318
// Read file does not emit event
319
changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-read-file');
320
await promises.readFile(anotherNewFilePath);
321
await Promise.race([timeout(100), changeFuture]);
322
323
// Stat file does not emit event
324
changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-stat');
325
await promises.stat(anotherNewFilePath);
326
await Promise.race([timeout(100), changeFuture]);
327
328
// Stat folder does not emit event
329
changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.UPDATED, 'unexpected-event-from-stat');
330
await promises.stat(copiedFolderpath);
331
await Promise.race([timeout(100), changeFuture]);
332
333
// Delete file
334
changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED);
335
disposables.add(instance.subscribe(copiedFilepath, change => subscriptions1.set(change.resource.fsPath, change.type)));
336
await promises.unlink(copiedFilepath);
337
await changeFuture;
338
assert.strictEqual(subscriptions1.get(copiedFilepath), FileChangeType.DELETED);
339
340
// Delete folder
341
changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED);
342
disposables.add(instance.subscribe(copiedFolderpath, change => subscriptions1.set(change.resource.fsPath, change.type)));
343
await promises.rmdir(copiedFolderpath);
344
await changeFuture;
345
assert.strictEqual(subscriptions1.get(copiedFolderpath), FileChangeType.DELETED);
346
347
disposables.dispose();
348
});
349
350
(isMacintosh /* this test seems not possible with fsevents backend */ ? test.skip : test)('basics (atomic writes)', async function () {
351
await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);
352
353
// Delete + Recreate file
354
const newFilePath = join(testDir, 'deep', 'conway.js');
355
const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED);
356
await promises.unlink(newFilePath);
357
Promises.writeFile(newFilePath, 'Hello Atomic World');
358
await changeFuture;
359
});
360
361
(!isLinux /* polling is only used in linux environments (WSL) */ ? test.skip : test)('basics (polling)', async function () {
362
await watcher.watch([{ path: testDir, excludes: [], pollingInterval: 100, recursive: true }]);
363
364
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
365
});
366
367
async function basicCrudTest(filePath: string, correlationId?: number | null, expectedCount?: number): Promise<void> {
368
369
// New file
370
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, correlationId, expectedCount);
371
await Promises.writeFile(filePath, 'Hello World');
372
await changeFuture;
373
374
// Change file
375
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, undefined, correlationId, expectedCount);
376
await Promises.writeFile(filePath, 'Hello Change');
377
await changeFuture;
378
379
// Delete file
380
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount);
381
await promises.unlink(filePath);
382
await changeFuture;
383
}
384
385
test('multiple events', async function () {
386
await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);
387
await promises.mkdir(join(testDir, 'deep-multiple'));
388
389
// multiple add
390
391
const newFilePath1 = join(testDir, 'newFile-1.txt');
392
const newFilePath2 = join(testDir, 'newFile-2.txt');
393
const newFilePath3 = join(testDir, 'newFile-3.txt');
394
const newFilePath4 = join(testDir, 'deep-multiple', 'newFile-1.txt');
395
const newFilePath5 = join(testDir, 'deep-multiple', 'newFile-2.txt');
396
const newFilePath6 = join(testDir, 'deep-multiple', 'newFile-3.txt');
397
398
const addedFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.ADDED);
399
const addedFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.ADDED);
400
const addedFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.ADDED);
401
const addedFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.ADDED);
402
const addedFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.ADDED);
403
const addedFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.ADDED);
404
405
await Promise.all([
406
await Promises.writeFile(newFilePath1, 'Hello World 1'),
407
await Promises.writeFile(newFilePath2, 'Hello World 2'),
408
await Promises.writeFile(newFilePath3, 'Hello World 3'),
409
await Promises.writeFile(newFilePath4, 'Hello World 4'),
410
await Promises.writeFile(newFilePath5, 'Hello World 5'),
411
await Promises.writeFile(newFilePath6, 'Hello World 6')
412
]);
413
414
await Promise.all([addedFuture1, addedFuture2, addedFuture3, addedFuture4, addedFuture5, addedFuture6]);
415
416
// multiple change
417
418
const changeFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.UPDATED);
419
const changeFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.UPDATED);
420
const changeFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.UPDATED);
421
const changeFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.UPDATED);
422
const changeFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.UPDATED);
423
const changeFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.UPDATED);
424
425
await Promise.all([
426
await Promises.writeFile(newFilePath1, 'Hello Update 1'),
427
await Promises.writeFile(newFilePath2, 'Hello Update 2'),
428
await Promises.writeFile(newFilePath3, 'Hello Update 3'),
429
await Promises.writeFile(newFilePath4, 'Hello Update 4'),
430
await Promises.writeFile(newFilePath5, 'Hello Update 5'),
431
await Promises.writeFile(newFilePath6, 'Hello Update 6')
432
]);
433
434
await Promise.all([changeFuture1, changeFuture2, changeFuture3, changeFuture4, changeFuture5, changeFuture6]);
435
436
// copy with multiple files
437
438
const copyFuture1 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-1.txt'), FileChangeType.ADDED);
439
const copyFuture2 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-2.txt'), FileChangeType.ADDED);
440
const copyFuture3 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy', 'newFile-3.txt'), FileChangeType.ADDED);
441
const copyFuture4 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy'), FileChangeType.ADDED);
442
443
await Promises.copy(join(testDir, 'deep-multiple'), join(testDir, 'deep-multiple-copy'), { preserveSymlinks: false });
444
445
await Promise.all([copyFuture1, copyFuture2, copyFuture3, copyFuture4]);
446
447
// multiple delete (single files)
448
449
const deleteFuture1 = awaitEvent(watcher, newFilePath1, FileChangeType.DELETED);
450
const deleteFuture2 = awaitEvent(watcher, newFilePath2, FileChangeType.DELETED);
451
const deleteFuture3 = awaitEvent(watcher, newFilePath3, FileChangeType.DELETED);
452
const deleteFuture4 = awaitEvent(watcher, newFilePath4, FileChangeType.DELETED);
453
const deleteFuture5 = awaitEvent(watcher, newFilePath5, FileChangeType.DELETED);
454
const deleteFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.DELETED);
455
456
await Promise.all([
457
await promises.unlink(newFilePath1),
458
await promises.unlink(newFilePath2),
459
await promises.unlink(newFilePath3),
460
await promises.unlink(newFilePath4),
461
await promises.unlink(newFilePath5),
462
await promises.unlink(newFilePath6)
463
]);
464
465
await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]);
466
467
// multiple delete (folder)
468
469
const deleteFolderFuture1 = awaitEvent(watcher, join(testDir, 'deep-multiple'), FileChangeType.DELETED);
470
const deleteFolderFuture2 = awaitEvent(watcher, join(testDir, 'deep-multiple-copy'), FileChangeType.DELETED);
471
472
await Promise.all([Promises.rm(join(testDir, 'deep-multiple'), RimRafMode.UNLINK), Promises.rm(join(testDir, 'deep-multiple-copy'), RimRafMode.UNLINK)]);
473
474
await Promise.all([deleteFolderFuture1, deleteFolderFuture2]);
475
});
476
477
test('subsequent watch updates watchers (path)', async function () {
478
await watcher.watch([{ path: testDir, excludes: [join(realpathSync(testDir), 'unrelated')], recursive: true }]);
479
480
// New file (*.txt)
481
let newTextFilePath = join(testDir, 'deep', 'newFile.txt');
482
let changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);
483
await Promises.writeFile(newTextFilePath, 'Hello World');
484
await changeFuture;
485
486
await watcher.watch([{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'unrelated')], recursive: true }]);
487
newTextFilePath = join(testDir, 'deep', 'newFile2.txt');
488
changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);
489
await Promises.writeFile(newTextFilePath, 'Hello World');
490
await changeFuture;
491
492
await watcher.watch([{ path: join(testDir, 'deep'), excludes: [realpathSync(testDir)], recursive: true }]);
493
await watcher.watch([{ path: join(testDir, 'deep'), excludes: [], recursive: true }]);
494
newTextFilePath = join(testDir, 'deep', 'newFile3.txt');
495
changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);
496
await Promises.writeFile(newTextFilePath, 'Hello World');
497
await changeFuture;
498
});
499
500
test('invalid path does not crash watcher', async function () {
501
await watcher.watch([
502
{ path: testDir, excludes: [], recursive: true },
503
{ path: join(testDir, 'invalid-folder'), excludes: [], recursive: true },
504
{ path: FileAccess.asFileUri('').fsPath, excludes: [], recursive: true }
505
]);
506
507
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
508
});
509
510
test('subsequent watch updates watchers (excludes)', async function () {
511
await watcher.watch([{ path: testDir, excludes: [realpathSync(testDir)], recursive: true }]);
512
await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);
513
514
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
515
});
516
517
test('subsequent watch updates watchers (includes)', async function () {
518
await watcher.watch([{ path: testDir, excludes: [], includes: ['nothing'], recursive: true }]);
519
await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);
520
521
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
522
});
523
524
test('includes are supported', async function () {
525
await watcher.watch([{ path: testDir, excludes: [], includes: ['**/deep/**'], recursive: true }]);
526
527
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
528
});
529
530
test('includes are supported (relative pattern explicit)', async function () {
531
await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'deep/newFile.txt' }], recursive: true }]);
532
533
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
534
});
535
536
test('includes are supported (relative pattern implicit)', async function () {
537
await watcher.watch([{ path: testDir, excludes: [], includes: ['deep/newFile.txt'], recursive: true }]);
538
539
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
540
});
541
542
test('excludes are supported (path)', async function () {
543
return testExcludes([join(realpathSync(testDir), 'deep')]);
544
});
545
546
test('excludes are supported (glob)', function () {
547
return testExcludes(['deep/**']);
548
});
549
550
async function testExcludes(excludes: string[]) {
551
await watcher.watch([{ path: testDir, excludes, recursive: true }]);
552
553
// New file (*.txt)
554
const newTextFilePath = join(testDir, 'deep', 'newFile.txt');
555
const changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED);
556
await Promises.writeFile(newTextFilePath, 'Hello World');
557
558
const res = await Promise.any([
559
timeout(500).then(() => true),
560
changeFuture.then(() => false)
561
]);
562
563
if (!res) {
564
assert.fail('Unexpected change event');
565
}
566
}
567
568
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () {
569
const link = join(testDir, 'deep-linked');
570
const linkTarget = join(testDir, 'deep');
571
await promises.symlink(linkTarget, link);
572
573
await watcher.watch([{ path: link, excludes: [], recursive: true }]);
574
575
return basicCrudTest(join(link, 'newFile.txt'));
576
});
577
578
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () {
579
const link = join(testDir, 'deep-linked');
580
const linkTarget = join(testDir, 'deep');
581
await promises.symlink(linkTarget, link);
582
583
await watcher.watch([{ path: testDir, excludes: [], recursive: true }, { path: link, excludes: [], recursive: true }]);
584
585
return basicCrudTest(join(link, 'newFile.txt'));
586
});
587
588
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support', async function () {
589
addUNCHostToAllowlist('localhost');
590
591
// Local UNC paths are in the form of: \\localhost\c$\my_dir
592
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;
593
594
await watcher.watch([{ path: uncPath, excludes: [], recursive: true }]);
595
596
return basicCrudTest(join(uncPath, 'deep', 'newFile.txt'));
597
});
598
599
(isLinux /* linux: is case sensitive */ ? test.skip : test)('wrong casing', async function () {
600
const deepWrongCasedPath = join(testDir, 'DEEP');
601
602
await watcher.watch([{ path: deepWrongCasedPath, excludes: [], recursive: true }]);
603
604
return basicCrudTest(join(deepWrongCasedPath, 'newFile.txt'));
605
});
606
607
test('invalid folder does not explode', async function () {
608
const invalidPath = join(testDir, 'invalid');
609
610
await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]);
611
});
612
613
(isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path without correlation restarts watching', async function () {
614
const watchedPath = join(testDir, 'deep');
615
616
await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]);
617
618
// Delete watched path and await
619
const warnFuture = awaitMessage(watcher, 'warn');
620
await Promises.rm(watchedPath, RimRafMode.UNLINK);
621
await warnFuture;
622
623
// Restore watched path
624
await timeout(1500); // node.js watcher used for monitoring folder restore is async
625
await promises.mkdir(watchedPath);
626
await timeout(1500); // restart is delayed
627
await watcher.whenReady();
628
629
// Verify events come in again
630
const newFilePath = join(watchedPath, 'newFile.txt');
631
const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);
632
await Promises.writeFile(newFilePath, 'Hello World');
633
await changeFuture;
634
});
635
636
test('correlationId is supported', async function () {
637
const correlationId = Math.random();
638
await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: true }]);
639
640
return basicCrudTest(join(testDir, 'newFile.txt'), correlationId);
641
});
642
643
test('should not exclude roots that do not overlap', async () => {
644
if (isWindows) {
645
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']);
646
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
647
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
648
} else {
649
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a']), ['/a']);
650
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']);
651
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
652
}
653
});
654
655
test('should remove sub-folders of other paths', async () => {
656
if (isWindows) {
657
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
658
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
659
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
660
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
661
} else {
662
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']);
663
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']);
664
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
665
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']);
666
}
667
});
668
669
test('should ignore when everything excluded', async () => {
670
assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []);
671
});
672
673
test('watching same or overlapping paths supported when correlation is applied', async () => {
674
await watcher.watch([
675
{ path: testDir, excludes: [], recursive: true, correlationId: 1 }
676
]);
677
678
await basicCrudTest(join(testDir, 'newFile.txt'), null, 1);
679
680
// same path, same options
681
await watcher.watch([
682
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
683
{ path: testDir, excludes: [], recursive: true, correlationId: 2, },
684
{ path: testDir, excludes: [], recursive: true, correlationId: undefined }
685
]);
686
687
await basicCrudTest(join(testDir, 'newFile.txt'), null, 3);
688
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 3);
689
690
// same path, different options
691
await watcher.watch([
692
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
693
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
694
{ path: testDir, excludes: [], recursive: true, correlationId: undefined },
695
{ path: testDir, excludes: [join(realpathSync(testDir), 'deep')], recursive: true, correlationId: 3 },
696
{ path: testDir, excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 4 },
697
]);
698
699
await basicCrudTest(join(testDir, 'newFile.txt'), null, 5);
700
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 5);
701
702
// overlapping paths (same options)
703
await watcher.watch([
704
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
705
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
706
{ path: join(testDir, 'deep'), excludes: [], recursive: true, correlationId: 3 },
707
]);
708
709
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
710
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
711
712
// overlapping paths (different options)
713
await watcher.watch([
714
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
715
{ path: testDir, excludes: [join(realpathSync(testDir), 'some')], recursive: true, correlationId: 2 },
716
{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 3 },
717
]);
718
719
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
720
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
721
});
722
723
test('watching missing path emits watcher fail event', async function () {
724
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
725
726
const folderPath = join(testDir, 'missing');
727
watcher.watch([{ path: folderPath, excludes: [], recursive: true }]);
728
729
await onDidWatchFail;
730
});
731
732
test('deleting watched path emits watcher fail and delete event if correlated', async function () {
733
const folderPath = join(testDir, 'deep');
734
735
await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);
736
737
let failed = false;
738
const instance = Array.from(watcher.watchers)[0];
739
assert.strictEqual(instance.include(folderPath), true);
740
instance.onDidFail(() => failed = true);
741
742
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
743
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1);
744
Promises.rm(folderPath, RimRafMode.UNLINK);
745
await onDidWatchFail;
746
await changeFuture;
747
assert.strictEqual(failed, true);
748
assert.strictEqual(instance.failed, true);
749
});
750
751
(!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 () => {
752
await testWatchFolderDoesNotExist(false);
753
});
754
755
test('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => {
756
await testWatchFolderDoesNotExist(true);
757
});
758
759
async function testWatchFolderDoesNotExist(reuseExistingWatcher: boolean) {
760
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
761
762
const folderPath = join(testDir, 'not-found');
763
764
const requests: IRecursiveWatchRequest[] = [];
765
if (reuseExistingWatcher) {
766
requests.push({ path: testDir, excludes: [], recursive: true });
767
await watcher.watch(requests);
768
}
769
770
const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true };
771
requests.push(request);
772
773
await watcher.watch(requests);
774
await onDidWatchFail;
775
776
if (reuseExistingWatcher) {
777
assert.strictEqual(watcher.isSuspended(request), true);
778
} else {
779
assert.strictEqual(watcher.isSuspended(request), 'polling');
780
}
781
782
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
783
let onDidWatch = Event.toPromise(watcher.onDidWatch);
784
await promises.mkdir(folderPath);
785
await changeFuture;
786
await onDidWatch;
787
788
assert.strictEqual(watcher.isSuspended(request), false);
789
790
const filePath = join(folderPath, 'newFile.txt');
791
await basicCrudTest(filePath);
792
793
if (!reuseExistingWatcher) {
794
onDidWatchFail = Event.toPromise(watcher.onWatchFail);
795
await Promises.rm(folderPath);
796
await onDidWatchFail;
797
798
changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
799
onDidWatch = Event.toPromise(watcher.onDidWatch);
800
await promises.mkdir(folderPath);
801
await changeFuture;
802
await onDidWatch;
803
804
await basicCrudTest(filePath);
805
}
806
}
807
808
(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => {
809
await testWatchFolderExists(false);
810
});
811
812
test('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => {
813
await testWatchFolderExists(true);
814
});
815
816
async function testWatchFolderExists(reuseExistingWatcher: boolean) {
817
const folderPath = join(testDir, 'deep');
818
819
const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true }];
820
if (reuseExistingWatcher) {
821
requests.push({ path: testDir, excludes: [], recursive: true });
822
}
823
824
await watcher.watch(requests);
825
826
const filePath = join(folderPath, 'newFile.txt');
827
await basicCrudTest(filePath);
828
829
if (!reuseExistingWatcher) {
830
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
831
await Promises.rm(folderPath);
832
await onDidWatchFail;
833
834
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
835
const onDidWatch = Event.toPromise(watcher.onDidWatch);
836
await promises.mkdir(folderPath);
837
await changeFuture;
838
await onDidWatch;
839
840
await basicCrudTest(filePath);
841
}
842
}
843
844
test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () {
845
const folderPath1 = join(testDir, 'deep', 'not-existing1');
846
const folderPath2 = join(testDir, 'deep', 'not-existing2');
847
const folderPath3 = join(testDir, 'not-existing3');
848
849
const requests: IRecursiveWatchRequest[] = [
850
{ path: folderPath1, excludes: [], recursive: true, correlationId: 1 },
851
{ path: folderPath2, excludes: [], recursive: true, correlationId: 2 },
852
{ path: folderPath3, excludes: [], recursive: true, correlationId: 3 },
853
{ path: join(testDir, 'deep'), excludes: [], recursive: true }
854
];
855
856
await watcher.watch(requests);
857
858
assert.strictEqual(watcher.isSuspended(requests[0]), true);
859
assert.strictEqual(watcher.isSuspended(requests[1]), true);
860
assert.strictEqual(watcher.isSuspended(requests[2]), 'polling');
861
assert.strictEqual(watcher.isSuspended(requests[3]), false);
862
});
863
864
test('event type filter', async function () {
865
const request = { path: testDir, excludes: [], recursive: true, filter: FileChangeFilter.ADDED | FileChangeFilter.DELETED, correlationId: 1 };
866
await watcher.watch([request]);
867
868
// Change file
869
const filePath = join(testDir, 'lorem-newfile.txt');
870
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, 1);
871
await Promises.writeFile(filePath, 'Hello Change');
872
await changeFuture;
873
874
// Delete file
875
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, 1);
876
await promises.unlink(filePath);
877
await changeFuture;
878
});
879
});
880
881