diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 59ff5889c3f..cbadfee1900 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -170,7 +170,7 @@ export class ExtensionsActivator implements IDisposable { private readonly _registry: ExtensionDescriptionRegistry; private readonly _resolvedExtensionsSet: Set; - private readonly _hostExtensionsMap: Map; + private readonly _externalExtensionsMap: Map; private readonly _host: IExtensionsActivatorHost; private readonly _activatingExtensions: Map>; private readonly _activatedExtensions: Map; @@ -182,7 +182,7 @@ export class ExtensionsActivator implements IDisposable { constructor( registry: ExtensionDescriptionRegistry, resolvedExtensions: ExtensionIdentifier[], - hostExtensions: ExtensionIdentifier[], + externalExtensions: ExtensionIdentifier[], host: IExtensionsActivatorHost, @ILogService private readonly _logService: ILogService ) { @@ -190,8 +190,8 @@ export class ExtensionsActivator implements IDisposable { this._registry = registry; this._resolvedExtensionsSet = new Set(); resolvedExtensions.forEach((extensionId) => this._resolvedExtensionsSet.add(ExtensionIdentifier.toKey(extensionId))); - this._hostExtensionsMap = new Map(); - hostExtensions.forEach((extensionId) => this._hostExtensionsMap.set(ExtensionIdentifier.toKey(extensionId), extensionId)); + this._externalExtensionsMap = new Map(); + externalExtensions.forEach((extensionId) => this._externalExtensionsMap.set(ExtensionIdentifier.toKey(extensionId), extensionId)); this._host = host; this._activatingExtensions = new Map>(); this._activatedExtensions = new Map(); @@ -248,7 +248,7 @@ export class ExtensionsActivator implements IDisposable { * semantics: `redExtensions` must wait for `greenExtensions`. */ private _handleActivateRequest(currentActivation: ActivationIdAndReason, greenExtensions: { [id: string]: ActivationIdAndReason }, redExtensions: ActivationIdAndReason[]): void { - if (this._hostExtensionsMap.has(ExtensionIdentifier.toKey(currentActivation.id))) { + if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(currentActivation.id))) { greenExtensions[ExtensionIdentifier.toKey(currentActivation.id)] = currentActivation; return; } @@ -299,11 +299,11 @@ export class ExtensionsActivator implements IDisposable { return; } - if (this._hostExtensionsMap.has(ExtensionIdentifier.toKey(depId))) { + if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(depId))) { // must first wait for the dependency to activate currentExtensionGetsGreenLight = false; greenExtensions[ExtensionIdentifier.toKey(depId)] = { - id: this._hostExtensionsMap.get(ExtensionIdentifier.toKey(depId))!, + id: this._externalExtensionsMap.get(ExtensionIdentifier.toKey(depId))!, reason: currentActivation.reason }; continue; diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts new file mode 100644 index 00000000000..91f286d238d --- /dev/null +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -0,0 +1,243 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; + +suite('ExtensionsActivator', () => { + + const idA = new ExtensionIdentifier(`a`); + const idB = new ExtensionIdentifier(`b`); + const idC = new ExtensionIdentifier(`c`); + + test('calls activate only once with sequential activations', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA) + ]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('calls activate only once with parallel activations', async () => { + const extActivation = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivation] + ]); + const activator = createActivator(host, [ + desc(idA, [], ['evt1', 'evt2']) + ]); + + const activate1 = activator.activateByEvent('evt1', false); + const activate2 = activator.activateByEvent('evt2', false); + + extActivation.resolve(); + + await activate1; + await activate2; + + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('activates dependencies first', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB], ['evt1']), + desc(idB, [], ['evt1']), + ]); + + const activate = activator.activateByEvent('evt1', false); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB]); + extActivationB.resolve(); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + extActivationA.resolve(); + + await timeout(0); + await activate; + + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + }); + + test('Supports having resolved extensions', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA, [idB]) + ], [idB]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('Supports having external extensions', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB]) + ], [], [idB]); + + const activate = activator.activateByEvent('*', false); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB]); + extActivationB.resolve(); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + extActivationA.resolve(); + + await activate; + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + }); + + test('Error: activateById with missing extension', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA), + desc(idB), + ]); + + let error: Error | undefined = undefined; + try { + await activator.activateById(idC, { startup: false, extensionId: idC, activationEvent: 'none' }); + } catch (err) { + error = err; + } + + assert.strictEqual(typeof error === 'undefined', false); + }); + + test('Error: dependency missing', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA, [idB]), + ]); + + await activator.activateByEvent('*', false); + + assert.deepStrictEqual(host.errors.length, 1); + assert.deepStrictEqual(host.errors[0][0], idA); + }); + + test('Error: dependency activation failed', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB]), + desc(idB) + ]); + + const activate = activator.activateByEvent('*', false); + extActivationB.reject(new Error(`b fails!`)); + + await activate; + assert.deepStrictEqual(host.errors.length, 2); + assert.deepStrictEqual(host.errors[0][0], idB); + assert.deepStrictEqual(host.errors[1][0], idA); + }); + + class SimpleExtensionsActivatorHost implements IExtensionsActivatorHost { + public readonly activateCalls: ExtensionIdentifier[] = []; + public readonly errors: [ExtensionIdentifier, Error | null, MissingExtensionDependency | null][] = []; + + onExtensionActivationError(extensionId: ExtensionIdentifier, error: Error | null, missingExtensionDependency: MissingExtensionDependency | null): void { + this.errors.push([extensionId, error, missingExtensionDependency]); + } + + actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { + this.activateCalls.push(extensionId); + return Promise.resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); + } + } + + class PromiseExtensionsActivatorHost extends SimpleExtensionsActivatorHost { + + constructor( + private readonly _promises: [ExtensionIdentifier, ExtensionActivationPromiseSource][] + ) { + super(); + } + + override actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { + this.activateCalls.push(extensionId); + for (const [id, promiseSource] of this._promises) { + if (id.value === extensionId.value) { + return promiseSource.promise; + } + } + throw new Error(`Unexpected!`); + } + } + + class ExtensionActivationPromiseSource { + private _resolve!: (value: ActivatedExtension) => void; + private _reject!: (err: Error) => void; + public readonly promise: Promise; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + public resolve(): void { + this._resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); + } + + public reject(err: Error): void { + this._reject(err); + } + } + + function createActivator(host: IExtensionsActivatorHost, extensionDescriptions: IExtensionDescription[], resolvedExtensions: ExtensionIdentifier[] = [], hostExtensions: ExtensionIdentifier[] = []): ExtensionsActivator { + const registry = new ExtensionDescriptionRegistry(extensionDescriptions); + return new ExtensionsActivator(registry, resolvedExtensions, hostExtensions, host, new NullLogService()); + } + + function desc(id: ExtensionIdentifier, deps: ExtensionIdentifier[] = [], activationEvents: string[] = ['*']): IExtensionDescription { + return { + name: id.value, + publisher: 'test', + version: '0.0.0', + engines: { vscode: '^1.0.0' }, + identifier: id, + extensionLocation: URI.parse(`nothing://nowhere`), + isBuiltin: false, + isUnderDevelopment: false, + isUserBuiltin: false, + activationEvents, + main: 'index.js', + extensionDependencies: deps.map(d => d.value) + }; + } + +});