Path: blob/main/src/vs/platform/files/test/browser/fileService.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 { DeferredPromise, timeout } from '../../../../base/common/async.js';7import { bufferToReadable, bufferToStream, VSBuffer } from '../../../../base/common/buffer.js';8import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';11import { isEqual } from '../../../../base/common/resources.js';12import { consumeStream, newWriteableStream, ReadableStreamEvents } from '../../../../base/common/stream.js';13import { URI } from '../../../../base/common/uri.js';14import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';15import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions, IFileChange, isFileSystemWatcher, FileChangesEvent, FileChangeType } from '../../common/files.js';16import { FileService } from '../../common/fileService.js';17import { NullFileSystemProvider } from '../common/nullFileSystemProvider.js';18import { NullLogService } from '../../../log/common/log.js';1920suite('File Service', () => {2122const disposables = new DisposableStore();2324teardown(() => {25disposables.clear();26});2728test('provider registration', async () => {29const service = disposables.add(new FileService(new NullLogService()));30const resource = URI.parse('test://foo/bar');31const provider = new NullFileSystemProvider();3233assert.strictEqual(await service.canHandleResource(resource), false);34assert.strictEqual(service.hasProvider(resource), false);35assert.strictEqual(service.getProvider(resource.scheme), undefined);3637const registrations: IFileSystemProviderRegistrationEvent[] = [];38disposables.add(service.onDidChangeFileSystemProviderRegistrations(e => {39registrations.push(e);40}));4142const capabilityChanges: IFileSystemProviderCapabilitiesChangeEvent[] = [];43disposables.add(service.onDidChangeFileSystemProviderCapabilities(e => {44capabilityChanges.push(e);45}));4647let registrationDisposable: IDisposable | undefined;48let callCount = 0;49disposables.add(service.onWillActivateFileSystemProvider(e => {50callCount++;5152if (e.scheme === 'test' && callCount === 1) {53e.join(new Promise(resolve => {54registrationDisposable = service.registerProvider('test', provider);5556resolve();57}));58}59}));6061assert.strictEqual(await service.canHandleResource(resource), true);62assert.strictEqual(service.hasProvider(resource), true);63assert.strictEqual(service.getProvider(resource.scheme), provider);6465assert.strictEqual(registrations.length, 1);66assert.strictEqual(registrations[0].scheme, 'test');67assert.strictEqual(registrations[0].added, true);68assert.ok(registrationDisposable);6970assert.strictEqual(capabilityChanges.length, 0);7172provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy);73assert.strictEqual(capabilityChanges.length, 1);74provider.setCapabilities(FileSystemProviderCapabilities.Readonly);75assert.strictEqual(capabilityChanges.length, 2);7677await service.activateProvider('test');78assert.strictEqual(callCount, 2); // activation is called again7980assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);81assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);8283registrationDisposable.dispose();8485assert.strictEqual(await service.canHandleResource(resource), false);86assert.strictEqual(service.hasProvider(resource), false);8788assert.strictEqual(registrations.length, 2);89assert.strictEqual(registrations[1].scheme, 'test');90assert.strictEqual(registrations[1].added, false);91});9293test('watch', async () => {94const service = disposables.add(new FileService(new NullLogService()));9596let disposeCounter = 0;97disposables.add(service.registerProvider('test', new NullFileSystemProvider(() => {98return toDisposable(() => {99disposeCounter++;100});101})));102await service.activateProvider('test');103104const resource1 = URI.parse('test://foo/bar1');105const watcher1Disposable = service.watch(resource1);106107await timeout(0); // service.watch() is async108assert.strictEqual(disposeCounter, 0);109watcher1Disposable.dispose();110assert.strictEqual(disposeCounter, 1);111112disposeCounter = 0;113const resource2 = URI.parse('test://foo/bar2');114const watcher2Disposable1 = service.watch(resource2);115const watcher2Disposable2 = service.watch(resource2);116const watcher2Disposable3 = service.watch(resource2);117118await timeout(0); // service.watch() is async119assert.strictEqual(disposeCounter, 0);120watcher2Disposable1.dispose();121assert.strictEqual(disposeCounter, 0);122watcher2Disposable2.dispose();123assert.strictEqual(disposeCounter, 0);124watcher2Disposable3.dispose();125assert.strictEqual(disposeCounter, 1);126127disposeCounter = 0;128const resource3 = URI.parse('test://foo/bar3');129const watcher3Disposable1 = service.watch(resource3);130const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });131const watcher3Disposable3 = service.watch(resource3, { recursive: false, excludes: [], includes: [] });132133await timeout(0); // service.watch() is async134assert.strictEqual(disposeCounter, 0);135watcher3Disposable1.dispose();136assert.strictEqual(disposeCounter, 1);137watcher3Disposable2.dispose();138assert.strictEqual(disposeCounter, 2);139watcher3Disposable3.dispose();140assert.strictEqual(disposeCounter, 3);141142service.dispose();143});144145test('watch - with corelation', async () => {146const service = disposables.add(new FileService(new NullLogService()));147148const provider = new class extends NullFileSystemProvider {149private readonly _testOnDidChangeFile = new Emitter<readonly IFileChange[]>();150override readonly onDidChangeFile: Event<readonly IFileChange[]> = this._testOnDidChangeFile.event;151152fireFileChange(changes: readonly IFileChange[]) {153this._testOnDidChangeFile.fire(changes);154}155};156157disposables.add(service.registerProvider('test', provider));158await service.activateProvider('test');159160const globalEvents: FileChangesEvent[] = [];161disposables.add(service.onDidFilesChange(e => {162globalEvents.push(e);163}));164165const watcher0 = disposables.add(service.watch(URI.parse('test://watch/folder1'), { recursive: true, excludes: [], includes: [] }));166assert.strictEqual(isFileSystemWatcher(watcher0), false);167const watcher1 = disposables.add(service.watch(URI.parse('test://watch/folder2'), { recursive: true, excludes: [], includes: [], correlationId: 100 }));168assert.strictEqual(isFileSystemWatcher(watcher1), true);169const watcher2 = disposables.add(service.watch(URI.parse('test://watch/folder3'), { recursive: true, excludes: [], includes: [], correlationId: 200 }));170assert.strictEqual(isFileSystemWatcher(watcher2), true);171172const watcher1Events: FileChangesEvent[] = [];173disposables.add(watcher1.onDidChange(e => {174watcher1Events.push(e);175}));176177const watcher2Events: FileChangesEvent[] = [];178disposables.add(watcher2.onDidChange(e => {179watcher2Events.push(e);180}));181182provider.fireFileChange([{ resource: URI.parse('test://watch/folder1'), type: FileChangeType.ADDED }]);183provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);184provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);185provider.fireFileChange([{ resource: URI.parse('test://watch/folder3/file'), type: FileChangeType.UPDATED, cId: 200 }]);186provider.fireFileChange([{ resource: URI.parse('test://watch/folder3'), type: FileChangeType.UPDATED, cId: 200 }]);187188provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 50 }]);189provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 60 }]);190provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 70 }]);191192assert.strictEqual(globalEvents.length, 1);193assert.strictEqual(watcher1Events.length, 2);194assert.strictEqual(watcher2Events.length, 2);195});196197test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {198testReadErrorBubbles(true);199});200201test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060)', async () => {202testReadErrorBubbles(false);203});204205async function testReadErrorBubbles(async: boolean) {206const service = disposables.add(new FileService(new NullLogService()));207208const provider = new class extends NullFileSystemProvider {209override async stat(resource: URI): Promise<IStat> {210return {211mtime: Date.now(),212ctime: Date.now(),213size: 100,214type: FileType.File215};216}217218override readFile(resource: URI): Promise<Uint8Array> {219if (async) {220return timeout(5, CancellationToken.None).then(() => { throw new Error('failed'); });221}222223throw new Error('failed');224}225226override open(resource: URI, opts: IFileOpenOptions): Promise<number> {227if (async) {228return timeout(5, CancellationToken.None).then(() => { throw new Error('failed'); });229}230231throw new Error('failed');232}233234override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {235if (async) {236const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);237timeout(5, CancellationToken.None).then(() => stream.error(new Error('failed')));238239return stream;240241}242243throw new Error('failed');244}245};246247disposables.add(service.registerProvider('test', provider));248249for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) {250provider.setCapabilities(capabilities);251252let e1;253try {254await service.readFile(URI.parse('test://foo/bar'));255} catch (error) {256e1 = error;257}258259assert.ok(e1);260261let e2;262try {263const stream = await service.readFileStream(URI.parse('test://foo/bar'));264await consumeStream(stream.value, chunk => chunk[0]);265} catch (error) {266e2 = error;267}268269assert.ok(e2);270}271}272273test('readFile/readFileStream supports cancellation (https://github.com/microsoft/vscode/issues/138805)', async () => {274const service = disposables.add(new FileService(new NullLogService()));275276let readFileStreamReady: DeferredPromise<void> | undefined = undefined;277278const provider = new class extends NullFileSystemProvider {279280override async stat(resource: URI): Promise<IStat> {281return {282mtime: Date.now(),283ctime: Date.now(),284size: 100,285type: FileType.File286};287}288289override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {290const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);291disposables.add(token.onCancellationRequested(() => {292stream.error(new Error('Expected cancellation'));293stream.end();294}));295296readFileStreamReady!.complete();297298return stream;299}300};301302disposables.add(service.registerProvider('test', provider));303304provider.setCapabilities(FileSystemProviderCapabilities.FileReadStream);305306let e1;307try {308const cts = new CancellationTokenSource();309readFileStreamReady = new DeferredPromise();310const promise = service.readFile(URI.parse('test://foo/bar'), undefined, cts.token);311await Promise.all([readFileStreamReady.p.then(() => cts.cancel()), promise]);312} catch (error) {313e1 = error;314}315316assert.ok(e1);317318let e2;319try {320const cts = new CancellationTokenSource();321readFileStreamReady = new DeferredPromise();322const stream = await service.readFileStream(URI.parse('test://foo/bar'), undefined, cts.token);323await Promise.all([readFileStreamReady.p.then(() => cts.cancel()), consumeStream(stream.value, chunk => chunk[0])]);324} catch (error) {325e2 = error;326}327328assert.ok(e2);329});330331test('enforced atomic read/write/delete', async () => {332const service = disposables.add(new FileService(new NullLogService()));333334const atomicResource = URI.parse('test://foo/bar/atomic');335const nonAtomicResource = URI.parse('test://foo/nonatomic');336337let atomicReadCounter = 0;338let atomicWriteCounter = 0;339let atomicDeleteCounter = 0;340341const provider = new class extends NullFileSystemProvider implements IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability {342343override async stat(resource: URI): Promise<IStat> {344return {345type: FileType.File,346ctime: Date.now(),347mtime: Date.now(),348size: 0349};350}351352override async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array> {353if (opts?.atomic) {354atomicReadCounter++;355}356return new Uint8Array();357}358359override readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {360return newWriteableStream<Uint8Array>(chunk => chunk[0]);361}362363enforceAtomicReadFile(resource: URI): boolean {364return isEqual(resource, atomicResource);365}366367override async writeFile(resource: URI, content: Uint8Array, opts: IFileAtomicWriteOptions): Promise<void> {368if (opts.atomic) {369atomicWriteCounter++;370}371}372373enforceAtomicWriteFile(resource: URI): IFileAtomicOptions | false {374return isEqual(resource, atomicResource) ? { postfix: '.tmp' } : false;375}376377override async delete(resource: URI, opts: IFileAtomicDeleteOptions): Promise<void> {378if (opts.atomic) {379atomicDeleteCounter++;380}381}382383enforceAtomicDelete(resource: URI): IFileAtomicOptions | false {384return isEqual(resource, atomicResource) ? { postfix: '.tmp' } : false;385}386};387388provider.setCapabilities(389FileSystemProviderCapabilities.FileReadWrite |390FileSystemProviderCapabilities.FileOpenReadWriteClose |391FileSystemProviderCapabilities.FileReadStream |392FileSystemProviderCapabilities.FileAtomicRead |393FileSystemProviderCapabilities.FileAtomicWrite |394FileSystemProviderCapabilities.FileAtomicDelete395);396397disposables.add(service.registerProvider('test', provider));398399await service.readFile(atomicResource);400await service.readFile(nonAtomicResource);401await service.readFileStream(atomicResource);402await service.readFileStream(nonAtomicResource);403404await service.writeFile(atomicResource, VSBuffer.fromString(''));405await service.writeFile(nonAtomicResource, VSBuffer.fromString(''));406407await service.writeFile(atomicResource, bufferToStream(VSBuffer.fromString('')));408await service.writeFile(nonAtomicResource, bufferToStream(VSBuffer.fromString('')));409410await service.writeFile(atomicResource, bufferToReadable(VSBuffer.fromString('')));411await service.writeFile(nonAtomicResource, bufferToReadable(VSBuffer.fromString('')));412413await service.del(atomicResource);414await service.del(nonAtomicResource);415416assert.strictEqual(atomicReadCounter, 2);417assert.strictEqual(atomicWriteCounter, 3);418assert.strictEqual(atomicDeleteCounter, 1);419});420421ensureNoDisposablesAreLeakedInTestSuite();422});423424425