@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
552 lines • 31.1 kB
JavaScript
;
// *****************************************************************************
// 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