UNPKG

@eclipse-emfcloud/model-service-theia

Version:
552 lines 31.1 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2023-2024 STMicroelectronics. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: MIT License which is // available at https://opensource.org/licenses/MIT. // // SPDX-License-Identifier: EPL-2.0 OR MIT // ***************************************************************************** var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const model_manager_1 = require("@eclipse-emfcloud/model-manager/lib/api/model-manager"); const model_service_1 = require("@eclipse-emfcloud/model-service"); const model_validation_1 = require("@eclipse-emfcloud/model-validation"); const core_1 = require("@theia/core"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const performance_1 = require("@theia/core/lib/node/performance"); const inversify_1 = require("@theia/core/shared/inversify"); const chai_1 = __importStar(require("chai")); const chai_as_promised_1 = __importDefault(require("chai-as-promised")); const lodash_1 = require("lodash"); const sinon_1 = __importDefault(require("sinon")); const model_hub_tracker_1 = require("../../common/model-hub-tracker"); const backend_module_1 = __importDefault(require("../backend-module")); const model_hub_lifecycle_contribution_1 = require("../model-hub-lifecycle-contribution"); const model_hub_manager_1 = require("../model-hub-manager"); const model_service_contribution_1 = require("../model-service-contribution"); chai_1.default.use(chai_as_promised_1.default); describe('DefaultModelHubManager', () => { const appContext = 'test-app'; let container; let modelHub; let sandbox; const contrib1 = { id: 'test.contrib1', setModelManager: () => undefined, setValidationService: () => undefined, setModelAccessorBus: () => undefined, validationContribution: { getValidators: () => [], }, }; let setModelHub1; beforeEach(() => { container = new inversify_1.Container(); container.load(backend_module_1.default); sandbox = sinon_1.default.createSandbox(); contrib1.setModelHub = setModelHub1 = sandbox.spy((hub) => (modelHub = hub)); container .bind(model_service_contribution_1.ModelServiceContribution) .toConstantValue(contrib1); const factory = container.get(model_hub_manager_1.DefaultModelHubManager); factory.createModelHub(appContext); }); afterEach(() => { sandbox.restore(); }); it('provides hub to contributions', () => { sinon_1.default.assert.calledWithMatch(setModelHub1, sinon_1.default.match.instanceOf(model_service_1.ModelHubImpl)); }); it('initializes model hub', function () { // Its being set is verified separately assumeThat(this, 'model hub not initialized', () => !!modelHub); if (modelHub) { (0, chai_1.expect)(modelHub.context).to.equal(appContext); (0, chai_1.expect)(modelHub) .to.haveOwnProperty('modelManager') .that.is.instanceOf(model_manager_1.ModelManagerImpl); (0, chai_1.expect)(modelHub) .to.haveOwnProperty('validationService') .that.is.instanceOf(model_validation_1.ModelValidationServiceImpl); } }); it('injects contributions into model hub', function () { // Its being set is verified separately assumeThat(this, 'model hub not initialized', () => !!modelHub); if (modelHub) { const contributions = modelHub.contributions; (0, chai_1.expect)(contributions.get(contrib1.id)).to.equal(contrib1); } }); describe('scoped model service contributions', () => { let modelHubManager; beforeEach(() => { container = new inversify_1.Container(); container.load(backend_module_1.default); const testModule = new inversify_1.ContainerModule((bind) => { bind(model_service_contribution_1.ModelServiceContribution).to(TestContribution); }); container.load(testModule); modelHubManager = container.get(model_hub_manager_1.DefaultModelHubManager); }); it('different contribution instances per model hub', () => { const modelHubA = modelHubManager.getModelHub('context-a'); const modelHubB = modelHubManager.getModelHub('context-b'); const contributionA = getContributions(modelHubA).get('testContribution'); const contributionB = getContributions(modelHubB).get('testContribution'); (0, chai_1.expect)(contributionA).to.exist; (0, chai_1.expect)(contributionB).to.exist; (0, chai_1.expect)(contributionA).to.not.equal(contributionB); }); it('different model service instances per model hub', () => { const modelHubA = modelHubManager.getModelHub('context-a'); const modelHubB = modelHubManager.getModelHub('context-b'); const modelServiceA = modelHubA.getModelService('testContribution'); const modelServiceB = modelHubB.getModelService('testContribution'); (0, chai_1.expect)(modelServiceA).to.exist; (0, chai_1.expect)(modelServiceB).to.exist; (0, chai_1.expect)(modelServiceA).to.not.equal(modelServiceB); }); }); describe('inversify DI', () => { const appContext1 = 'app-a'; const appContext2 = 'app-b'; const appContext3 = 'app-c'; let container; let modelHubManager; beforeEach(() => { container = new inversify_1.Container(); container.load(backend_module_1.default); modelHubManager = container.get(model_hub_manager_1.ModelHubManager); }); it('getModelHub()', () => { const hub1 = modelHubManager.getModelHub(appContext1); (0, chai_1.expect)(hub1).to.exist; const hub2 = modelHubManager.getModelHub(appContext2); (0, chai_1.expect)(hub2).to.exist; const hub3 = modelHubManager.getModelHub(appContext3); (0, chai_1.expect)(hub3).to.exist; (0, chai_1.expect)(hub1).not.to.equal(hub2); (0, chai_1.expect)(hub1).not.to.equal(hub3); (0, chai_1.expect)(hub2).not.to.equal(hub3); const hub1Again = modelHubManager.getModelHub(appContext1); (0, chai_1.expect)(hub1Again).to.equal(hub1); }); it('disposeContext()', function () { const hub1 = modelHubManager.getModelHub(appContext1); const hub2 = modelHubManager.getModelHub(appContext2); const hub3 = modelHubManager.getModelHub(appContext3); const allHubs = () => Array.from(modelHubManager.modelHubs.values()).map((record) => record.modelHub); (0, chai_1.expect)(allHubs()).to.have.members([hub1, hub2, hub3]); modelHubManager.disposeContext(appContext2); (0, chai_1.expect)(allHubs()).to.have.members([hub1, hub3]); (0, chai_1.expect)(allHubs()).not.to.include(hub2); // doesn't hurt to do it again modelHubManager.disposeContext(appContext2); (0, chai_1.expect)(allHubs()).to.have.members([hub1, hub3]); (0, chai_1.expect)(allHubs()).not.to.include(hub2); }); describe('hub lifecycle contribution providers', () => { let TestModelHubLifecycle = class TestModelHubLifecycle { getPriority() { return 42; } createModelHub(...args) { return new model_service_1.ModelHubImpl(...args); } async initializeModelHub(_modelHub) { return void undefined; } disposeModelHub(modelHub) { modelHub.dispose(); } }; TestModelHubLifecycle = __decorate([ (0, inversify_1.injectable)() ], TestModelHubLifecycle); beforeEach(() => { container = new inversify_1.Container(); container.bind(TestModelHubLifecycle).toSelf().inSingletonScope(); (0, core_1.bindContribution)(container, TestModelHubLifecycle, [ model_hub_lifecycle_contribution_1.ModelHubLifecycleContribution, ]); container.load(backend_module_1.default); modelHubManager = container.get(model_hub_manager_1.ModelHubManager); }); describe('consults priorities', () => { it('priority wins', () => { const createSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'createModelHub'); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(createSpy).to.have.returned(hub); }); it('priority defaulted', () => { const lifecycle = container.get(TestModelHubLifecycle); lifecycle.getPriority = undefined; const createSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'createModelHub'); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(createSpy).to.have.returned(hub); }); it('opt out via NaN', () => { const createSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'createModelHub'); sandbox .stub(TestModelHubLifecycle.prototype, 'getPriority') .returns(NaN); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(hub).to.exist; (0, chai_1.expect)(createSpy).not.to.have.been.called; }); }); describe('initialization', () => { it('initializer not provided', async () => { const lifecycle = container.get(TestModelHubLifecycle); lifecycle.initializeModelHub = undefined; const hub = modelHubManager.getModelHub('some-context'); return (0, chai_1.expect)(modelHubManager.initializeContext('some-context')).to.eventually.be.equal(hub); }); it('initializer provided', async () => { const initializeSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'initializeModelHub'); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(hub).to.exist; await (0, chai_1.expect)(modelHubManager.initializeContext('some-context')).to.eventually.be.equal(hub); (0, chai_1.expect)(initializeSpy).to.have.been.calledWithExactly(hub); }); it('initialize wrong context', async () => { modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(modelHubManager.initializeContext('other-context')).to .eventually.be.rejected; }); describe('concurrent initializations', () => { let initialHubCount; let expectedHubCount; beforeEach(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any initialHubCount = (0, lodash_1.get)(modelHubManager, 'modelHubs.size'); expectedHubCount = initialHubCount + 1; }); afterEach(() => { (0, chai_1.expect)(modelHubManager).to.have.nested.property('modelHubs.size', expectedHubCount, 'wrong number of model hubs remaining after test'); }); it('initialize only once', async () => { const initializeSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'initializeModelHub'); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(hub).to.exist; await Promise.all([ modelHubManager.initializeContext('some-context'), modelHubManager.initializeContext('some-context'), modelHubManager.initializeContext('some-context'), ]); (0, chai_1.expect)(initializeSpy).to.have.been.calledOnce; }); it('all provisions wait', async () => { const initializeSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'initializeModelHub'); const modelHubs = await Promise.all([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); (0, chai_1.expect)(initializeSpy).to.have.been.calledOnce; // all provisions got the same hub (0, chai_1.expect)(modelHubs).to.have.length(3); for (let i = 1; i < modelHubs.length; i++) { (0, chai_1.expect)(modelHubs[i]).to.be.equal(modelHubs[i - 1]); } }); it('timeout waiting for initialization', async function () { // Failed hub should be disposed expectedHubCount = initialHubCount; const testTimeout = this.timeout(); sandbox .stub(TestModelHubLifecycle.prototype, 'initializeModelHub') .callsFake(() => (0, promise_util_1.wait)(testTimeout / 4)); Object.assign(modelHubManager, { initializationTimeoutMs: testTimeout / 8, }); const modelHubs = await Promise.allSettled([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); // all provisions timed out (0, chai_1.expect)(modelHubs).to.have.length(3); for (const hub of modelHubs) { (0, chai_1.expect)(hub.status).to.be.equal('rejected'); (0, chai_1.expect)(hub).to.have.nested.property('reason.message', 'Model Hub initialization timed out for context: some-context'); } }); it('failed initialization', async function () { // Failed hub should be disposed expectedHubCount = initialHubCount; Object.assign(modelHubManager, { initializationTimeoutMs: 125, }); sandbox .stub(TestModelHubLifecycle.prototype, 'initializeModelHub') .callsFake(() => Promise.reject(new Error('Boom!'))); const modelHubs = await Promise.allSettled([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); // all provisions errored out (0, chai_1.expect)(modelHubs).to.have.length(3); for (const hub of modelHubs) { (0, chai_1.expect)(hub.status).to.be.equal('rejected'); (0, chai_1.expect)(hub).to.have.nested.property('reason.message', 'Boom!'); } }); it('eventual success of initialization', async function () { Object.assign(modelHubManager, { initializationTimeoutMs: 300, }); const initializeStub = sandbox .stub(TestModelHubLifecycle.prototype, 'initializeModelHub') .onFirstCall() .callsFake(() => Promise.reject(new Error('Boom!'))) .onSecondCall() .callsFake(() => Promise.reject(new Error('Boom!'))) .callsFake(() => Promise.resolve()); const modelHubs = await Promise.all([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); (0, chai_1.expect)(initializeStub).to.have.been.calledThrice; // all provisions got the same hub (0, chai_1.expect)(modelHubs).to.have.length(3); (0, chai_1.expect)(modelHubs[0]).to.be.instanceOf(model_service_1.ModelHubImpl); for (let i = 1; i < modelHubs.length; i++) { (0, chai_1.expect)(modelHubs[i]).to.be.equal(modelHubs[i - 1]); } }); }); describe('initialization performance timing', () => { let stopwatch; beforeEach(() => { stopwatch = sandbox.createStubInstance(performance_1.NodeStopwatch); stopwatch.start.callsFake(() => ({ log: sandbox.stub(), error: sandbox.stub(), })); Object.assign(modelHubManager, { stopwatch }); }); it('successful initialization', async () => { const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(hub).to.exist; await Promise.all([ modelHubManager.initializeContext('some-context'), modelHubManager.initializeContext('some-context'), modelHubManager.initializeContext('some-context'), ]); (0, chai_1.expect)(stopwatch.start).to.have.been.calledOnceWith('initialize model hub'); const measurement = stopwatch.start.returnValues[0]; // Wait for microtasks spawned by the initialization to run await (0, promise_util_1.wait)(1); (0, chai_1.expect)(measurement.log).to.have.been.calledOnceWith('complete'); }); it('timeout waiting for initialization', async function () { const testTimeout = this.timeout(); sandbox .stub(TestModelHubLifecycle.prototype, 'initializeModelHub') .callsFake(() => (0, promise_util_1.wait)(testTimeout / 4)); Object.assign(modelHubManager, { initializationTimeoutMs: testTimeout / 8, }); await Promise.allSettled([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); // provision timed out (0, chai_1.expect)(stopwatch.start).to.have.been.calledOnceWith('initialize model hub'); const measurement = stopwatch.start.returnValues[0]; (0, chai_1.expect)(measurement.error).to.have.been.calledOnceWith('timed out'); }); it('failed initialization', async function () { Object.assign(modelHubManager, { initializationTimeoutMs: 100, }); sandbox .stub(TestModelHubLifecycle.prototype, 'initializeModelHub') .callsFake(() => Promise.reject(new Error('Boom!'))); await Promise.allSettled([ modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), modelHubManager.provideModelHub('some-context'), ]); // provision errored out (0, chai_1.expect)(stopwatch.start).to.have.been.calledOnceWith('initialize model hub'); const measurement = stopwatch.start.returnValues[0]; (0, chai_1.expect)(measurement.error).to.have.been.calledOnceWith('failed', sinon_1.default.match.instanceOf(Error)); }); }); describe('model hub tracking', () => { let tracker; beforeEach(() => { tracker = container.get(model_hub_tracker_1.ModelHubTracker); }); it('isModelHubAvailable()', async () => { const sub = tracker.trackModelHubs(); sub.onModelHubCreated = sandbox.stub(); (0, chai_1.expect)(tracker.isModelHubAvailable('new-context')).to.be.false; await modelHubManager.provideModelHub('new-context'); (0, chai_1.expect)(tracker.isModelHubAvailable('new-context')).to.be.true; }); it('notifies hub creation', async () => { const sub = tracker.trackModelHubs(); sub.onModelHubCreated = sandbox.stub(); await modelHubManager.provideModelHub('new-context'); (0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledWith('new-context'); }); it('notifies extant hubs', async () => { await modelHubManager.provideModelHub('new-context'); const sub = tracker.trackModelHubs(); sub.onModelHubCreated = sandbox.stub(); (0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledWith('new-context'); }); it('notifies pending hubs later', async () => { const futureHub = modelHubManager.provideModelHub('new-context'); const sub = tracker.trackModelHubs(); sub.onModelHubCreated = sandbox.stub(); (0, chai_1.expect)(sub.onModelHubCreated).not.to.have.been.called; await futureHub; (0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledWith('new-context'); }); it('unset creation call-back', async () => { const callback = sandbox.stub(); await modelHubManager.provideModelHub('new-context'); const sub = tracker.trackModelHubs(); sub.onModelHubCreated = callback; (0, chai_1.expect)(callback).to.have.been.calledWith('new-context'); sub.onModelHubCreated = undefined; await modelHubManager.provideModelHub('second-context'); (0, chai_1.expect)(callback).to.have.been.calledOnce; (0, chai_1.expect)(callback).not.to.have.been.calledWith('second-context'); }); it('notifies hub destruction', async () => { await modelHubManager.provideModelHub('new-context'); const sub = tracker.trackModelHubs(); sub.onModelHubDestroyed = sandbox.stub(); modelHubManager.disposeContext('new-context'); (0, chai_1.expect)(sub.onModelHubDestroyed).to.have.been.calledWith('new-context'); }); it('close tracking subscription', async () => { const sub = tracker.trackModelHubs(); sub.onModelHubCreated = sandbox.stub(); sub.onModelHubDestroyed = sandbox.stub(); await modelHubManager.provideModelHub('new-context'); sub.close(); modelHubManager.disposeContext('new-context'); (0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledOnce; (0, chai_1.expect)(sub.onModelHubDestroyed).not.to.have.been.called; }); it('redundant close of tracking subscription', async () => { const sub = tracker.trackModelHubs(); sub.onModelHubDestroyed = sandbox.stub(); await modelHubManager.provideModelHub('new-context'); sub.close(); sub.close(); // Again, doesn't matter modelHubManager.disposeContext('new-context'); (0, chai_1.expect)(sub.onModelHubDestroyed).not.to.have.been.called; }); it('empty subscription is harmless', async () => { const sub = tracker.trackModelHubs(); await modelHubManager.provideModelHub('new-context'); modelHubManager.disposeContext('new-context'); sub.close(); }); }); }); describe('disposal', () => { it('disposer not provided', async () => { const lifecycle = container.get(TestModelHubLifecycle); lifecycle.disposeModelHub = undefined; const hub = modelHubManager.getModelHub('some-context'); const disposeSpy = sandbox.spy(hub, 'dispose'); modelHubManager.disposeContext('some-context'); (0, chai_1.expect)(disposeSpy).to.have.been.called; }); it('disposer provided', async () => { const disposeSpy = sandbox.spy(TestModelHubLifecycle.prototype, 'disposeModelHub'); const hub = modelHubManager.getModelHub('some-context'); (0, chai_1.expect)(hub).to.exist; modelHubManager.disposeContext('some-context'); (0, chai_1.expect)(disposeSpy).to.have.been.calledWithExactly(hub); }); it('dispose wrong context', async () => { const hub = modelHubManager.getModelHub('some-context'); const disposeSpy = sandbox.spy(hub, 'dispose'); modelHubManager.disposeContext('other-context'); (0, chai_1.expect)(disposeSpy).not.to.have.been.called; }); }); }); }); }); const assumeThat = (context, reason, predicate) => { if (!predicate()) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion context.test.title += ' - ' + reason; context.skip(); } }; let TestContribution = class TestContribution extends model_service_1.AbstractModelServiceContribution { constructor() { super(); this.modelService = {}; this.initialize({ id: 'testContribution', }); } getModelService() { return this.modelService; } }; TestContribution = __decorate([ (0, inversify_1.injectable)(), __metadata("design:paramtypes", []) ], TestContribution); function getContributions(modelHub) { return modelHub.contributions; } //# sourceMappingURL=model-hub-manager.spec.js.map