@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
327 lines • 15.6 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_manager_1 = require("@eclipse-emfcloud/model-manager");
const model_service_1 = require("@eclipse-emfcloud/model-service");
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_like_1 = __importDefault(require("chai-like"));
const sinon_1 = __importDefault(require("sinon"));
const sinon_chai_1 = __importDefault(require("sinon-chai"));
const common_1 = require("../../common");
const backend_module_1 = __importDefault(require("../backend-module"));
const model_hub_provider_1 = require("../model-hub-provider");
const model_service_contribution_1 = require("../model-service-contribution");
const fake_json_rpc_1 = require("./fake-json-rpc");
chai_1.default.use(chai_like_1.default);
chai_1.default.use(sinon_chai_1.default);
const MODEL1_ID = 'test.model1';
function createTestContainer(...contributions) {
const container = new inversify_1.Container();
container.load(backend_module_1.default);
contributions.forEach((contrib) => container.bind(model_service_contribution_1.ModelServiceContribution).toConstantValue(contrib));
(0, fake_json_rpc_1.bindFakeRpcConnectionFactory)(container);
return container;
}
describe('ModelHubServer', () => {
const appContext = 'test-app';
let sandbox;
let contrib1;
let modelHubServer;
let clientProxy;
let modelHub;
beforeEach(async () => {
sandbox = sinon_1.default.createSandbox();
contrib1 = new Contribution1();
const container = createTestContainer(contrib1);
const factory = container.get(fake_json_rpc_1.RpcConnectionFactory);
clientProxy = {
onModelChanged: sandbox.stub(),
onModelDirtyState: sandbox.stub(),
onModelValidated: sandbox.stub(),
onModelLoaded: sandbox.stub(),
onModelUnloaded: sandbox.stub(),
onModelHubDisposed: sandbox.stub(),
closeSubscription: sandbox.stub(),
onModelHubCreated: sandbox.stub(),
onModelHubDestroyed: sandbox.stub(),
};
modelHubServer = await factory.getServer(common_1.ModelHubProtocolServicePath, clientProxy);
// Some tests need to start interacting with the model service
// right away, so initialize the context's provided hub
modelHub = await container.get(model_hub_provider_1.ModelHubProvider)(appContext);
});
afterEach(() => {
sandbox.restore();
});
it('provides a model', async () => {
const model = await modelHubServer.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(model).to.be.like({ name: 'Model 1' });
});
it('validates a model', async () => {
const model = await modelHubServer.getModel(appContext, MODEL1_ID);
model.name = 'NoSpace';
let diagnostic;
diagnostic = await modelHubServer.validateModels(appContext, MODEL1_ID);
(0, chai_1.expect)(diagnostic).to.be.like({ severity: 'error', path: '/name' });
model.name = 'Has Space';
// Haven't revalidated, yet
diagnostic = await modelHubServer.getValidationState(appContext, MODEL1_ID);
(0, chai_1.expect)(diagnostic).to.be.like({ severity: 'error', path: '/name' });
diagnostic = await modelHubServer.validateModels(appContext, MODEL1_ID);
(0, chai_1.expect)(diagnostic).to.be.eql((0, model_validation_1.ok)());
});
it('can undo and redo', async () => {
const service = contrib1.getModelService();
(0, chai_1.expect)(service).to.exist;
await service?.setName('New Name');
let model = await modelHubServer.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(model).to.be.like({ name: 'New Name' });
const undone = await modelHubServer.undo(appContext, MODEL1_ID);
(0, chai_1.expect)(undone).to.be.true;
model = await modelHubServer.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(model).to.be.like({ name: 'Model 1' });
const redone = await modelHubServer.redo(appContext, MODEL1_ID);
(0, chai_1.expect)(redone).to.be.true;
model = await modelHubServer.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(model).to.be.like({ name: 'New Name' });
});
it('manages dirty state', async () => {
let dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.false;
const service = contrib1.getModelService();
(0, chai_1.expect)(service).to.exist;
await service?.setName('New Name');
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.true;
await modelHubServer.undo(appContext, MODEL1_ID);
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.false;
await modelHubServer.redo(appContext, MODEL1_ID);
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.true;
const saved = await modelHubServer.save(appContext, MODEL1_ID);
(0, chai_1.expect)(saved).to.be.true;
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.false;
await modelHubServer.undo(appContext, MODEL1_ID);
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.true;
const flushed = await modelHubServer.flush(appContext, MODEL1_ID);
(0, chai_1.expect)(flushed).to.be.true;
dirty = await modelHubServer.isDirty(appContext, MODEL1_ID);
(0, chai_1.expect)(dirty).to.be.true;
// Cannot redo after flush
const redone = await modelHubServer.redo(appContext, MODEL1_ID);
(0, chai_1.expect)(redone).to.be.false;
});
describe('manages subscriptions', async () => {
let token;
beforeEach(async () => {
token = await modelHubServer.subscribe(appContext, MODEL1_ID);
const service = contrib1.getModelService();
(0, chai_1.expect)(service).to.exist;
await service?.setName('NewName');
});
it('creates the subscription', () => {
(0, chai_1.expect)(token).to.exist;
(0, chai_1.expect)(token.id).to.be.greaterThan(0);
(0, chai_1.expect)(token.modelIds).to.be.eql([MODEL1_ID]);
});
it('notifies model changed', () => {
sinon_1.default.assert.calledOnceWithMatch(clientProxy.onModelChanged, token.id, MODEL1_ID, [
{ op: 'test', path: '/name', value: 'Model 1' },
{ op: 'replace', path: '/name', value: 'NewName' },
]);
});
it('notifies model validation', async () => {
await modelHubServer.validateModels(appContext, MODEL1_ID);
// It was already called once with an OK result on creation of the model
sinon_1.default.assert.calledWithMatch(clientProxy.onModelValidated, token.id, MODEL1_ID, sinon_1.default.match({
severity: 'error',
path: '/name',
}));
});
it('notifies dirty state', async () => {
sinon_1.default.assert.calledWithMatch(clientProxy.onModelDirtyState, token.id, MODEL1_ID, true);
await modelHubServer.save(appContext, MODEL1_ID);
sinon_1.default.assert.calledWithMatch(clientProxy.onModelDirtyState, token.id, MODEL1_ID, false);
});
it('notifies model loaded', async () => {
sinon_1.default.assert.calledWithMatch(clientProxy.onModelLoaded, token.id, MODEL1_ID);
});
it('notifies model unloaded', async () => {
const modelManager = modelHub.modelManager;
modelManager.removeModel(MODEL1_ID);
sinon_1.default.assert.calledWithMatch(clientProxy.onModelUnloaded, token.id, MODEL1_ID);
});
it('notifies hub disposal', () => {
modelHub.dispose();
sinon_1.default.assert.calledWithMatch(clientProxy.onModelHubDisposed, token.id);
});
it('can be closed', async () => {
await modelHubServer.closeSubscription(token);
sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id);
// Second attempt is harmless and without effect
await modelHubServer.closeSubscription(token);
(0, chai_1.expect)(clientProxy.closeSubscription.callCount).to.be.equal(1);
});
});
describe('disposal scenarios', () => {
// The main state to clean up is the subscriptions
let token;
beforeEach(async () => {
token = await modelHubServer.subscribe(appContext, MODEL1_ID);
});
it('dispose the server', () => {
modelHubServer.dispose();
sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id);
});
it('delete the client', async () => {
modelHubServer.setClient(undefined);
(0, chai_1.expect)(modelHubServer.getClient()).not.to.exist;
sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id);
// After this point, subscriptions don't do anything
const newSub = await modelHubServer.subscribe(appContext, MODEL1_ID);
const service = contrib1.getModelService();
(0, chai_1.expect)(service).to.exist;
await service?.setName('New Name');
sinon_1.default.assert.neverCalledWithMatch(clientProxy.onModelChanged, newSub.id, sinon_1.default.match.any, sinon_1.default.match.any);
});
it('setting the same client has no effect', () => {
modelHubServer.setClient(clientProxy);
sinon_1.default.assert.notCalled(clientProxy.closeSubscription);
});
});
describe('model hub tracking', () => {
it('notifies model hub creation', async () => {
await modelHubServer.getModel(appContext, MODEL1_ID);
(0, chai_1.expect)(clientProxy.onModelHubCreated).to.have.been.calledWith(appContext);
});
it('notifies model hub creation', async () => {
await modelHubServer.getModel(appContext, MODEL1_ID);
modelHub.dispose();
(0, chai_1.expect)(clientProxy.onModelHubDestroyed).to.have.been.calledWith(appContext);
});
});
});
class Contribution1 extends model_service_1.AbstractModelServiceContribution {
constructor() {
super();
this.modelService = {
getModel: () => {
let model = this.modelManager.getModel(MODEL1_ID);
if (!model) {
this.modelManager.setModel(MODEL1_ID, this.model);
model = this.modelManager.getModel(MODEL1_ID);
}
if (!model) {
throw new Error('No model to edit');
}
return model;
},
setName: async (newName) => {
this.modelService.getModel(); // Ensure the model is loaded
const command = createSetNameCommand(MODEL1_ID, newName);
await this.commandStack.execute(command);
},
};
this.model = {
name: 'Model 1',
};
this.initialize({
id: 'test.contrib1',
persistenceContribution: {
canHandle: (modelId) => Promise.resolve(modelId === MODEL1_ID),
loadModel: (modelId) => modelId === MODEL1_ID
? Promise.resolve(this.model)
: Promise.reject(new Error(`No such model: ${modelId}`)),
saveModel: async (modelId, model) => {
if (modelId !== MODEL1_ID) {
throw new Error(`Unsupported model ${modelId}`);
}
const newImage = { ...model };
Array.from(Object.keys(this.model)).forEach((key) => delete this.model[key]);
Object.assign(this.model, newImage);
return true;
},
},
validationContribution: {
getValidators: () => [
{
validate: async (_modelId, model) => {
if (typeof model.name !== 'string') {
return errorDiagnostic('No name.', '/name');
}
if (!model.name.includes(' ')) {
return errorDiagnostic('No space in the name.', '/name');
}
return (0, model_validation_1.ok)();
},
},
],
},
});
}
getModelService() {
return this.modelService;
}
setModelManager(modelManager) {
super.setModelManager(modelManager);
this.commandStack = modelManager.getCommandStack(MODEL1_ID);
}
}
function errorDiagnostic(message, path) {
return {
message,
path,
severity: 'error',
source: 'test',
};
}
function createSetNameCommand(modelId, newName) {
return (0, model_manager_1.createModelUpdaterCommand)('Set Name', modelId, (model) => {
model.name = newName;
});
}
//# sourceMappingURL=model-hub-server.spec.js.map