@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
414 lines • 19 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 __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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const model_validation_1 = require("@eclipse-emfcloud/model-validation");
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 cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
const sinon_1 = __importDefault(require("sinon"));
const model_hub_tracker_1 = require("../../common/model-hub-tracker");
const frontend_model_hub_subscriber_1 = require("../frontend-model-hub-subscriber");
const fake_model_hub_protocol_1 = require("./fake-model-hub-protocol");
const test_module_1 = require("./test-module");
chai_1.default.use(chai_as_promised_1.default);
function createTestContainer() {
const container = new inversify_1.Container();
container.load(test_module_1.testModule);
return container;
}
const MODEL1_ID = 'test.model1';
const MODEL1 = { name: 'Model 1' };
describe('FrontendModelHubSubscriber', () => {
const appContext = 'test-app';
let sandbox;
let fake;
let subscriber;
let tracker;
let onModelChanged;
let onModelDirtyState;
let onModelValidated;
let onModelLoaded;
let onModelUnloaded;
beforeEach(async () => {
sandbox = sinon_1.default.createSandbox();
const container = createTestContainer();
subscriber = container.get(frontend_model_hub_subscriber_1.FrontendModelHubSubscriber);
tracker = container.get(model_hub_tracker_1.ModelHubTracker);
fake = container.get(fake_model_hub_protocol_1.FakeModelHubProtocol);
fake.setModel(MODEL1_ID, MODEL1);
(0, fake_model_hub_protocol_1.connectClient)(fake, subscriber);
onModelChanged = sandbox.stub();
onModelDirtyState = sandbox.stub();
onModelValidated = sandbox.stub();
onModelLoaded = sandbox.stub();
onModelUnloaded = sandbox.stub();
});
const modelChangeCases = [
['specific sub', [MODEL1_ID]],
['universal sub', []],
];
describe('notifies model change', () => {
modelChangeCases.forEach(([title, modelIds]) => {
it(title, async () => {
const sub = await subscriber.subscribe(appContext, ...modelIds);
sub.onModelChanged = onModelChanged;
const patch = [
{ op: 'replace', path: '/name', value: MODEL1.name },
];
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelChanged, MODEL1_ID, MODEL1, patch);
});
});
});
const dirtyStateCases = [
['specific sub', [MODEL1_ID]],
['universal sub', []],
];
describe('notifies dirty state', () => {
dirtyStateCases.forEach(([title, modelIds]) => {
it(title, async () => {
const sub = await subscriber.subscribe(appContext, ...modelIds);
sub.onModelDirtyState = onModelDirtyState;
fake.fakeModelDirtyState(MODEL1_ID, true);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelDirtyState, MODEL1_ID, MODEL1, true);
});
});
});
const validationCases = [
['specific sub', [MODEL1_ID]],
['universal sub', []],
];
describe('notifies model validation', () => {
validationCases.forEach(([title, modelIds]) => {
it(title, async () => {
const sub = await subscriber.subscribe(appContext, ...modelIds);
sub.onModelValidated = onModelValidated;
const diagnostic = {
message: 'This is a test',
path: '/name',
severity: 'error',
source: 'test',
};
fake.fakeModelValidated(MODEL1_ID, diagnostic);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelValidated, MODEL1_ID, MODEL1, diagnostic);
});
});
});
const modelLoadedCases = [
['specific sub', [MODEL1_ID]],
['universal sub', []],
];
describe('notifies model loaded', () => {
modelLoadedCases.forEach(([title, modelIds]) => {
it(title, async () => {
const sub = await subscriber.subscribe(appContext, ...modelIds);
sub.onModelLoaded = onModelLoaded;
fake.fakeModelLoaded(MODEL1_ID);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelLoaded, MODEL1_ID);
});
});
});
const modelUnloadedCases = [
['specific sub', [MODEL1_ID]],
['universal sub', []],
];
describe('notifies model unloaded', () => {
modelUnloadedCases.forEach(([title, modelIds]) => {
it(title, async () => {
const sub = await subscriber.subscribe(appContext, ...modelIds);
sub.onModelUnloaded = onModelUnloaded;
fake.fakeModelUnloaded(MODEL1_ID);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelUnloaded, MODEL1_ID, MODEL1);
});
});
});
describe('closes subscriptions', () => {
const patch = [
{ op: 'replace', path: '/name', value: MODEL1.name },
];
let sub;
beforeEach(async () => {
sub = await subscriber.subscribe(appContext, MODEL1_ID);
sub.onModelChanged = onModelChanged;
fake.fakeModelChange(MODEL1_ID, patch);
return asyncsResolved();
});
it('from the frontend', async () => {
sub.close();
await asyncsResolved();
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
(0, chai_1.expect)(onModelChanged.callCount).to.be.equal(1);
});
it('from the backend', async () => {
fake.fakeSubscriptionClosed(MODEL1_ID);
await asyncsResolved();
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
(0, chai_1.expect)(onModelChanged.callCount).to.be.equal(1);
});
it('is idempotent', async () => {
sub.close();
try {
sub.close();
sub.close();
}
catch (error) {
chai_1.assert.fail('Should have been OK to close subscription again.');
}
await asyncsResolved();
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
(0, chai_1.expect)(onModelChanged.callCount).to.be.equal(1);
});
});
describe('non-interference from other remote subscriptions', () => {
it('notifies model change', async () => {
const foreignSub = await fake.subscribe(appContext, MODEL1_ID);
const sub = await subscriber.subscribe(appContext, MODEL1_ID);
sub.onModelChanged = onModelChanged;
let patch = [
{ op: 'replace', path: '/name', value: MODEL1.name },
];
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelChanged, MODEL1_ID, MODEL1, patch);
// This mustn't close the frontend-subscriber
await fake.closeSubscription(foreignSub);
patch = [{ op: 'replace', path: '/name', value: 'another name' }];
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
sinon_1.default.assert.calledWithMatch(onModelChanged, MODEL1_ID, MODEL1, patch);
});
it('closing in the other order', async () => {
const foreignSub = await fake.subscribe(appContext, MODEL1_ID);
// Hack into the subscriber for its subscriptions
const subscriptions = subscriber.subscriptions;
const sub = await subscriber.subscribe(appContext, MODEL1_ID);
(0, chai_1.expect)(subscriptions).to.include(sub);
fake.closeSubscription(2); // This is the subscriber's self-subscription
// Closing the self-sub cleans up because now there are no
// notifications to forward
(0, chai_1.expect)(subscriptions).not.to.include(sub);
// But this is harmless
await fake.closeSubscription(foreignSub);
(0, chai_1.expect)(subscriptions).not.to.include(sub);
});
});
describe('corner cases', () => {
it("doesn't blow up on unassigned call-back", async () => {
await subscriber.subscribe(appContext, MODEL1_ID);
fake.fakeModelChange(MODEL1_ID, []);
fake.fakeModelDirtyState(MODEL1_ID, true);
fake.fakeModelValidated(MODEL1_ID, (0, model_validation_1.ok)());
fake.fakeModelLoaded(MODEL1_ID);
fake.fakeModelUnloaded(MODEL1_ID);
fake.fakeModelHubDisposed();
await asyncsResolved();
});
it("doesn't blow up on subscription gone AWOL", async () => {
await subscriber.subscribe(appContext, MODEL1_ID);
await asyncsResolved();
// Hack out the subscription
subscriber.subscriptions.length = 0;
fake.fakeModelChange(MODEL1_ID, []);
fake.fakeModelDirtyState(MODEL1_ID, true);
fake.fakeModelValidated(MODEL1_ID, (0, model_validation_1.ok)());
fake.fakeModelLoaded(MODEL1_ID);
fake.fakeModelUnloaded(MODEL1_ID);
fake.fakeModelHubDisposed();
await asyncsResolved();
});
it("doesn't blow up on subscription pipeline gone AWOL", async () => {
await subscriber.subscribe(appContext, MODEL1_ID);
await asyncsResolved();
// Hack out the subscription pipeline
subscriber.subscriptionPipelines.delete(appContext);
fake.fakeModelChange(MODEL1_ID, []);
fake.fakeModelDirtyState(MODEL1_ID, true);
fake.fakeModelValidated(MODEL1_ID, (0, model_validation_1.ok)());
fake.fakeModelLoaded(MODEL1_ID);
fake.fakeModelUnloaded(MODEL1_ID);
fake.fakeModelHubDisposed();
await asyncsResolved();
});
it("doesn't blow up on bad patch", async () => {
await subscriber.subscribe(appContext, MODEL1_ID);
await asyncsResolved();
const original = (0, cloneDeep_1.default)(await subscriber.getModel(appContext, MODEL1_ID));
const warn = sandbox.stub(console, 'warn');
// Issue a bad patch
fake.fakeModelChange(MODEL1_ID, [
{ op: 'test', path: '/nonesuch', value: '@@impossible@@' },
{ op: 'replace', path: '/name', value: 'NEW NAME' },
]);
await asyncsResolved();
(0, chai_1.expect)(warn).to.have.been.calledWithMatch('Error applying received model delta');
const current = await subscriber.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(current).to.eql(original);
});
it('close the self-subscription at the backend', async () => {
const sub = await subscriber.subscribe(appContext, MODEL1_ID);
sub.onModelChanged = onModelChanged;
fake.closeSubscription(1); // This is the subscriber's self-subscription
const patch = [
{ op: 'replace', path: '/name', value: MODEL1.name },
];
fake.fakeModelChange(MODEL1_ID, patch);
await asyncsResolved();
sinon_1.default.assert.notCalled(onModelChanged);
});
});
describe('model cache', () => {
it('updates by self-subscription', async () => {
const model = await subscriber.getModel(appContext, MODEL1_ID);
// Issue a patch
fake.fakeModelChange(MODEL1_ID, [
{ op: 'replace', path: '/name', value: 'NEW NAME' },
]);
await asyncsResolved();
(0, chai_1.expect)(model.name).to.equal('NEW NAME');
});
it('cleans up self-subscription', async () => {
const original = (0, cloneDeep_1.default)(await subscriber.getModel(appContext, MODEL1_ID));
subscriber.setModelHub(new fake_model_hub_protocol_1.FakeModelHubProtocol());
// Issue a patch
fake.fakeModelChange(MODEL1_ID, [
{ op: 'replace', path: '/name', value: 'NEW NAME' },
]);
await asyncsResolved();
const current = await subscriber.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(current).to.eql(original);
});
it('purges by self-subscription', async () => {
subscriber.setModelHub(new fake_model_hub_protocol_1.FakeModelHubProtocol());
// Remove the model
fake.removeModel(MODEL1_ID);
await asyncsResolved();
return (0, chai_1.expect)(subscriber.getModel(appContext, MODEL1_ID)).eventually.to.be
.rejected;
});
});
describe('model hub tracking', () => {
it('isModelHubAvailable()', () => {
(0, chai_1.expect)(tracker.isModelHubAvailable('new-context')).to.be.false;
fake.fakeModelHubCreated('new-context');
(0, chai_1.expect)(tracker.isModelHubAvailable('new-context')).to.be.true;
});
it('notifies model hub creation', () => {
const sub = tracker.trackModelHubs();
sub.onModelHubCreated = sinon_1.default.stub();
fake.fakeModelHubCreated('new-context');
(0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledWith('new-context');
});
it('notifies extant model hubs', () => {
fake.fakeModelHubCreated('new-context');
const sub = tracker.trackModelHubs();
sub.onModelHubCreated = sinon_1.default.stub();
(0, chai_1.expect)(sub.onModelHubCreated).to.have.been.calledWith('new-context');
});
it('unset creation call-back', async () => {
const callback = sandbox.stub();
fake.fakeModelHubCreated('new-context');
const sub = tracker.trackModelHubs();
sub.onModelHubCreated = callback;
(0, chai_1.expect)(callback).to.have.been.calledWith('new-context');
sub.onModelHubCreated = undefined;
fake.fakeModelHubCreated('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 model hub destruction', () => {
const sub = tracker.trackModelHubs();
sub.onModelHubDestroyed = sinon_1.default.stub();
fake.fakeModelHubDestroyed('new-context');
(0, chai_1.expect)(sub.onModelHubDestroyed).to.have.been.calledWith('new-context');
});
it('close subscription', () => {
const sub = tracker.trackModelHubs();
sub.onModelHubCreated = sinon_1.default.stub();
sub.onModelHubDestroyed = sinon_1.default.stub();
fake.fakeModelHubCreated('new-context');
sub.close();
fake.fakeModelHubDestroyed('new-context');
(0, chai_1.expect)(sub.onModelHubCreated).to.have.been.called;
(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();
fake.fakeModelHubCreated('new-context');
sub.close();
sub.close(); // Again, doesn't matter
fake.fakeModelHubDestroyed('new-context');
(0, chai_1.expect)(sub.onModelHubDestroyed).not.to.have.been.called;
});
it('empty tracking subscription is harmless', () => {
const sub = tracker.trackModelHubs();
fake.fakeModelHubCreated('new-context');
fake.fakeModelHubDestroyed('new-context');
sub.close();
});
it('notifies model hub destruction', () => {
const sub = tracker.trackModelHubs();
sub.onModelHubDestroyed = sinon_1.default.stub();
fake.fakeModelHubDestroyed('new-context');
(0, chai_1.expect)(sub.onModelHubDestroyed).to.have.been.calledWith('new-context');
});
});
});
function asyncsResolved() {
// It only takes until the next tick because in the test fixture
// the promises involved are all a priori resolved
return new Promise((resolve) => {
setImmediate(() => {
resolve();
});
});
}
//# sourceMappingURL=frontend-model-hub-subscriber.spec.js.map