UNPKG

@eclipse-emfcloud/model-service-theia

Version:
414 lines 19 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 __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