vscode/src/vs/platform/files/test/node/recursiveWatcher.integrationTest.ts

626 lines
25 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { realpathSync } from 'fs';
import { tmpdir } from 'os';
import { timeout } from 'vs/base/common/async';
import { dirname, join, sep } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { Promises, RimRafMode } from 'vs/base/node/pfs';
import { flakySuite, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileChangeType } from 'vs/platform/files/common/files';
import { IWatcher, ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/parcelWatcherService';
import { IWatchRequest } from 'vs/platform/files/common/watcher';
flakySuite('Recursive Watcher (parcel)', () => {
class TestParcelWatcherService extends ParcelWatcherService {
testNormalizePaths(paths: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatchRequest[] = paths.map(path => {
return { path, excludes: [] };
});
return this.normalizeRequests(requests).map(request => request.path);
}
override async watch(requests: IWatchRequest[]): Promise<void> {
await super.watch(requests);
await this.whenReady();
}
async whenReady(): Promise<void> {
for (const [, watcher] of this.watchers) {
await watcher.ready;
}
}
override toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined {
return super.toExcludePaths(path, excludes);
}
override restartWatching(watcher: IWatcher, delay = 10): void {
return super.restartWatching(watcher, delay);
}
}
let testDir: string;
let service: TestParcelWatcherService;
let loggingEnabled = false;
function enableLogging(enable: boolean) {
loggingEnabled = enable;
service?.setVerboseLogging(enable);
}
enableLogging(false);
setup(async () => {
service = new TestParcelWatcherService();
service.onDidLogMessage(e => {
if (loggingEnabled) {
console.log(`[recursive watcher test message] ${e.message}`);
}
});
service.onDidError(e => {
if (loggingEnabled) {
console.log(`[recursive watcher test error] ${e}`);
}
});
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher');
const sourceDir = getPathFromAmdModule(require, './fixtures/service');
await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });
});
teardown(async () => {
await service.stop();
// Possible that the file watcher is still holding
// onto the folders on Windows specifically and the
// unlink would fail. In that case, do not fail the
// test suite.
return Promises.rm(testDir).catch(error => console.error(error));
});
function toMsg(type: FileChangeType): string {
switch (type) {
case FileChangeType.ADDED: return 'added';
case FileChangeType.DELETED: return 'deleted';
default: return 'changed';
}
}
async function awaitEvent(service: TestParcelWatcherService, path: string, type: FileChangeType, failOnEventReason?: string): Promise<void> {
if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
}
// Await the event
await new Promise<void>((resolve, reject) => {
const disposable = service.onDidChangeFile(events => {
for (const event of events) {
if (event.path === path && event.type === type) {
disposable.dispose();
if (failOnEventReason) {
reject(new Error(`Unexpected file event: ${failOnEventReason}`));
} else {
resolve();
}
break;
}
}
});
});
// Unwind from the event call stack: we have seen crashes in Parcel
// when e.g. calling `unsubscribe` directly from the stack of a file
// change event
// Refs: https://github.com/microsoft/vscode/issues/137430
await timeout(1);
}
function awaitMessage(service: TestParcelWatcherService, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {
if (loggingEnabled) {
console.log(`Awaiting message of type ${type}`);
}
// Await the message
return new Promise<void>(resolve => {
const disposable = service.onDidLogMessage(msg => {
if (msg.type === type) {
disposable.dispose();
resolve();
}
});
});
}
test('basics', async function () {
await service.watch([{ path: testDir, excludes: [] }]);
// New file
const newFilePath = join(testDir, 'deep', 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
// New folder
const newFolderPath = join(testDir, 'deep', 'New Folder');
changeFuture = awaitEvent(service, newFolderPath, FileChangeType.ADDED);
await Promises.mkdir(newFolderPath);
await changeFuture;
// Rename file
let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt');
changeFuture = Promise.all([
awaitEvent(service, newFilePath, FileChangeType.DELETED),
awaitEvent(service, renamedFilePath, FileChangeType.ADDED)
]);
await Promises.rename(newFilePath, renamedFilePath);
await changeFuture;
// Rename folder
let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder');
changeFuture = Promise.all([
awaitEvent(service, newFolderPath, FileChangeType.DELETED),
awaitEvent(service, renamedFolderPath, FileChangeType.ADDED)
]);
await Promises.rename(newFolderPath, renamedFolderPath);
await changeFuture;
// Rename file (same name, different case)
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
changeFuture = Promise.all([
awaitEvent(service, renamedFilePath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFilePath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFilePath, caseRenamedFilePath);
await changeFuture;
renamedFilePath = caseRenamedFilePath;
// Rename folder (same name, different case)
const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder');
changeFuture = Promise.all([
awaitEvent(service, renamedFolderPath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFolderPath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFolderPath, caseRenamedFolderPath);
await changeFuture;
renamedFolderPath = caseRenamedFolderPath;
// Move file
const movedFilepath = join(testDir, 'movedFile.txt');
changeFuture = Promise.all([
awaitEvent(service, renamedFilePath, FileChangeType.DELETED),
awaitEvent(service, movedFilepath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFilePath, movedFilepath);
await changeFuture;
// Move folder
const movedFolderpath = join(testDir, 'Moved Folder');
changeFuture = Promise.all([
awaitEvent(service, renamedFolderPath, FileChangeType.DELETED),
awaitEvent(service, movedFolderpath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFolderPath, movedFolderpath);
await changeFuture;
// Copy file
const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt');
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.ADDED);
await Promises.copyFile(movedFilepath, copiedFilepath);
await changeFuture;
// Copy folder
const copiedFolderpath = join(testDir, 'deep', 'Copied Folder');
changeFuture = awaitEvent(service, copiedFolderpath, FileChangeType.ADDED);
await Promises.copy(movedFolderpath, copiedFolderpath, { preserveSymlinks: false });
await changeFuture;
// Change file
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED);
await Promises.writeFile(copiedFilepath, 'Hello Change');
await changeFuture;
// Create new file
const anotherNewFilePath = join(testDir, 'deep', 'anotherNewFile.txt');
changeFuture = awaitEvent(service, anotherNewFilePath, FileChangeType.ADDED);
await Promises.writeFile(anotherNewFilePath, 'Hello Another World');
await changeFuture;
// Skip following asserts on macOS where the fs-events service
// does not really give a full guarantee about the correlation
// of an event to a change.
if (!isMacintosh) {
// Read file does not emit event
changeFuture = awaitEvent(service, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-read-file');
await Promises.readFile(anotherNewFilePath);
await Promise.race([timeout(100), changeFuture]);
// Stat file does not emit event
changeFuture = awaitEvent(service, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-stat');
await Promises.stat(anotherNewFilePath);
await Promise.race([timeout(100), changeFuture]);
// Stat folder does not emit event
changeFuture = awaitEvent(service, copiedFolderpath, FileChangeType.UPDATED, 'unexpected-event-from-stat');
await Promises.stat(copiedFolderpath);
await Promise.race([timeout(100), changeFuture]);
}
// Delete file
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.DELETED);
await Promises.unlink(copiedFilepath);
await changeFuture;
// Delete folder
changeFuture = awaitEvent(service, copiedFolderpath, FileChangeType.DELETED);
await Promises.rmdir(copiedFolderpath);
await changeFuture;
});
(!isLinux /* polling is only used in linux environments (WSL) */ ? test.skip : test)('basics (polling)', async function () {
await service.watch([{ path: testDir, excludes: [], pollingInterval: 100 }]);
// New file
const newFilePath = join(testDir, 'deep', 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
// Change file
changeFuture = awaitEvent(service, newFilePath, FileChangeType.UPDATED);
await Promises.writeFile(newFilePath, 'Hello Change');
await changeFuture;
// Delete file
changeFuture = awaitEvent(service, newFilePath, FileChangeType.DELETED);
await Promises.unlink(newFilePath);
await changeFuture;
});
test('multiple events', async function () {
await service.watch([{ path: testDir, excludes: [] }]);
await Promises.mkdir(join(testDir, 'deep-multiple'));
// multiple add
const newFilePath1 = join(testDir, 'newFile-1.txt');
const newFilePath2 = join(testDir, 'newFile-2.txt');
const newFilePath3 = join(testDir, 'newFile-3.txt');
const newFilePath4 = join(testDir, 'deep-multiple', 'newFile-1.txt');
const newFilePath5 = join(testDir, 'deep-multiple', 'newFile-2.txt');
const newFilePath6 = join(testDir, 'deep-multiple', 'newFile-3.txt');
const addedFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.ADDED);
const addedFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.ADDED);
const addedFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.ADDED);
const addedFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.ADDED);
const addedFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.ADDED);
const addedFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.ADDED);
await Promise.all([
await Promises.writeFile(newFilePath1, 'Hello World 1'),
await Promises.writeFile(newFilePath2, 'Hello World 2'),
await Promises.writeFile(newFilePath3, 'Hello World 3'),
await Promises.writeFile(newFilePath4, 'Hello World 4'),
await Promises.writeFile(newFilePath5, 'Hello World 5'),
await Promises.writeFile(newFilePath6, 'Hello World 6')
]);
await Promise.all([addedFuture1, addedFuture2, addedFuture3, addedFuture4, addedFuture5, addedFuture6]);
// multiple change
const changeFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.UPDATED);
const changeFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.UPDATED);
const changeFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.UPDATED);
const changeFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.UPDATED);
const changeFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.UPDATED);
const changeFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.UPDATED);
await Promise.all([
await Promises.writeFile(newFilePath1, 'Hello Update 1'),
await Promises.writeFile(newFilePath2, 'Hello Update 2'),
await Promises.writeFile(newFilePath3, 'Hello Update 3'),
await Promises.writeFile(newFilePath4, 'Hello Update 4'),
await Promises.writeFile(newFilePath5, 'Hello Update 5'),
await Promises.writeFile(newFilePath6, 'Hello Update 6')
]);
await Promise.all([changeFuture1, changeFuture2, changeFuture3, changeFuture4, changeFuture5, changeFuture6]);
// copy with multiple files
const copyFuture1: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-1.txt'), FileChangeType.ADDED);
const copyFuture2: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-2.txt'), FileChangeType.ADDED);
const copyFuture3: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-3.txt'), FileChangeType.ADDED);
const copyFuture4: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.ADDED);
await Promises.copy(join(testDir, 'deep-multiple'), join(testDir, 'deep-multiple-copy'), { preserveSymlinks: false });
await Promise.all([copyFuture1, copyFuture2, copyFuture3, copyFuture4]);
// multiple delete (single files)
const deleteFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.DELETED);
const deleteFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.DELETED);
const deleteFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.DELETED);
const deleteFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.DELETED);
const deleteFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.DELETED);
const deleteFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.DELETED);
await Promise.all([
await Promises.unlink(newFilePath1),
await Promises.unlink(newFilePath2),
await Promises.unlink(newFilePath3),
await Promises.unlink(newFilePath4),
await Promises.unlink(newFilePath5),
await Promises.unlink(newFilePath6)
]);
await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]);
// multiple delete (folder)
const deleteFolderFuture1: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple'), FileChangeType.DELETED);
const deleteFolderFuture2: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.DELETED);
await Promise.all([Promises.rm(join(testDir, 'deep-multiple'), RimRafMode.UNLINK), Promises.rm(join(testDir, 'deep-multiple-copy'), RimRafMode.UNLINK)]);
await Promise.all([deleteFolderFuture1, deleteFolderFuture2]);
});
test('subsequent watch updates watchers (path)', async function () {
await service.watch([{ path: testDir, excludes: [join(realpathSync(testDir), 'unrelated')] }]);
// New file (*.txt)
let newTextFilePath = join(testDir, 'deep', 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
await service.watch([{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'unrelated')] }]);
newTextFilePath = join(testDir, 'deep', 'newFile2.txt');
changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
await service.watch([{ path: join(testDir, 'deep'), excludes: [realpathSync(testDir)] }]);
await service.watch([{ path: join(testDir, 'deep'), excludes: [] }]);
newTextFilePath = join(testDir, 'deep', 'newFile3.txt');
changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
});
test('subsequent watch updates watchers (excludes)', async function () {
await service.watch([{ path: testDir, excludes: [realpathSync(testDir)] }]);
await service.watch([{ path: testDir, excludes: [] }]);
// New file (*.txt)
let newTextFilePath = join(testDir, 'deep', 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
});
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () {
const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep');
await Promises.symlink(linkTarget, link);
await service.watch([{ path: link, excludes: [] }]);
// New file
const newFilePath = join(link, 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
});
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () {
const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep');
await Promises.symlink(linkTarget, link);
await service.watch([{ path: testDir, excludes: [] }, { path: link, excludes: [] }]);
// New file
const newFilePath = join(link, 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
});
(isLinux /* linux: is case sensitive */ ? test.skip : test)('wrong casing', async function () {
const deepWrongCasedPath = join(testDir, 'DEEP');
await service.watch([{ path: deepWrongCasedPath, excludes: [] }]);
// New file
const newFilePath = join(deepWrongCasedPath, 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
});
test('invalid folder does not explode', async function () {
const invalidPath = join(testDir, 'invalid');
await service.watch([{ path: invalidPath, excludes: [] }]);
});
test('deleting watched path is handled properly', async function () {
const watchedPath = join(testDir, 'deep');
await service.watch([{ path: watchedPath, excludes: [] }]);
// Delete watched path and await
const warnFuture = awaitMessage(service, 'warn');
await Promises.rm(watchedPath, RimRafMode.UNLINK);
await warnFuture;
// Restore watched path
await Promises.mkdir(watchedPath);
await timeout(1500); // restart is delayed
await service.whenReady();
// Verify events come in again
const newFilePath = join(watchedPath, 'newFile.txt');
const changeFuture = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
});
test('should not exclude roots that do not overlap', () => {
if (isWindows) {
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a']), ['C:\\a']);
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assert.deepStrictEqual(service.testNormalizePaths(['/a']), ['/a']);
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other paths', () => {
if (isWindows) {
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizePaths(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
} else {
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b']), ['/a']);
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizePaths(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
test('excludes are converted to absolute paths', () => {
// undefined / empty
assert.strictEqual(service.toExcludePaths(testDir, undefined), undefined);
assert.strictEqual(service.toExcludePaths(testDir, []), undefined);
// absolute paths
let excludes = service.toExcludePaths(testDir, [testDir]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, [`${testDir}${sep}`, join(testDir, 'foo', 'bar'), `${join(testDir, 'other', 'deep')}${sep}`]);
assert.strictEqual(excludes?.length, 3);
assert.strictEqual(excludes[0], testDir);
assert.strictEqual(excludes[1], join(testDir, 'foo', 'bar'));
assert.strictEqual(excludes[2], join(testDir, 'other', 'deep'));
// wrong casing is normalized for root
if (!isLinux) {
excludes = service.toExcludePaths(testDir, [join(testDir.toUpperCase(), 'node_modules', '**')]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
}
// exclude ignored if not parent of watched dir
excludes = service.toExcludePaths(testDir, [join(dirname(testDir), 'node_modules', '**')]);
assert.strictEqual(excludes, undefined);
// relative paths
excludes = service.toExcludePaths(testDir, ['.']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['foo', `bar${sep}`, join('foo', 'bar'), `${join('other', 'deep')}${sep}`]);
assert.strictEqual(excludes?.length, 4);
assert.strictEqual(excludes[0], join(testDir, 'foo'));
assert.strictEqual(excludes[1], join(testDir, 'bar'));
assert.strictEqual(excludes[2], join(testDir, 'foo', 'bar'));
assert.strictEqual(excludes[3], join(testDir, 'other', 'deep'));
// simple globs (relative)
excludes = service.toExcludePaths(testDir, ['**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['**/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['**\\**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['**/node_modules/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['**/.git/objects/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
excludes = service.toExcludePaths(testDir, ['**/node_modules']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['**/.git/objects']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
excludes = service.toExcludePaths(testDir, ['node_modules/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['.git/objects/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
// simple globs (absolute)
excludes = service.toExcludePaths(testDir, [join(testDir, 'node_modules', '**')]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
// Linux: more restrictive glob treatment
if (isLinux) {
excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
}
// unsupported globs
else {
excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']);
assert.strictEqual(excludes, undefined);
}
excludes = service.toExcludePaths(testDir, ['**/*.js']);
assert.strictEqual(excludes, undefined);
excludes = service.toExcludePaths(testDir, ['*.js']);
assert.strictEqual(excludes, undefined);
excludes = service.toExcludePaths(testDir, ['*']);
assert.strictEqual(excludes, undefined);
});
});