@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
426 lines (380 loc) • 12.7 kB
text/typescript
// *****************************************************************************
// 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
// *****************************************************************************
import { ModelAccessorBus } from '@eclipse-emfcloud/model-accessor-bus';
import {
CommandStack,
ModelManager,
SimpleCommand,
createModelUpdaterCommand,
} from '@eclipse-emfcloud/model-manager';
import {
AbstractModelServiceContribution,
HubAwareProvider,
ModelHub,
ModelServiceContribution,
} from '@eclipse-emfcloud/model-service';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { Operation } from 'fast-json-patch';
import sinon from 'sinon';
import {
ModelAccessorBusClient,
ModelAccessorBusProtocolServicePath,
} from '../../common';
import backendModule from '../backend-module';
import { ModelAccessorBusServer } from '../model-accessor-bus-server';
import { ModelHubProvider } from '../model-hub-provider';
import { ModelServiceContribution as ModelServiceContributionIdentifier } from '../model-service-contribution';
import {
RpcConnectionFactory,
bindFakeRpcConnectionFactory,
} from './fake-json-rpc';
type FakeModel = Record<string, unknown>;
const FAKE_MODEL_ID = 'test.fake-model';
function createTestContainer(contrib: ModelServiceContribution): Container {
const container = new Container();
container.load(backendModule);
container.bind(ModelServiceContributionIdentifier).toConstantValue(contrib);
bindFakeRpcConnectionFactory(container);
return container;
}
type FullModelAccessorBusServer = ModelAccessorBusServer<string> & {
getModelAccessorBus: (context: string) => Promise<ModelAccessorBus>;
};
describe('Model Accessor Bus Server', () => {
const appContext = 'test-app';
let sandbox: sinon.SinonSandbox;
let modelAccessorBusServer: ModelAccessorBusServer;
let fakeContribution: FakeContribution;
let clientProxy: {
[key in keyof ModelAccessorBusClient]: sinon.SinonSpy<
Parameters<ModelAccessorBusClient[key]>
>;
};
beforeEach(async () => {
sandbox = sinon.createSandbox();
fakeContribution = new FakeContribution();
const container = createTestContainer(fakeContribution);
const factory = container.get(RpcConnectionFactory);
clientProxy = {
onAccessorChanged: sandbox.stub(),
closeSubscription: sandbox.stub(),
};
modelAccessorBusServer = await factory.getServer<ModelAccessorBusServer>(
ModelAccessorBusProtocolServicePath,
clientProxy
);
// Some tests need to start interacting with the model service
// right away, so initialize the context's provided hub
await container.get<ModelHubProvider>(ModelHubProvider)(appContext);
});
afterEach(() => {
sandbox.restore();
});
describe('getClient', async () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('expect not undefined', async () => {
expect(modelAccessorBusServer.getClient()).not.to.be.undefined;
});
it('expect undefined', async () => {
modelAccessorBusServer.setClient(undefined);
expect(modelAccessorBusServer.getClient()).to.be.undefined;
});
it('expect specific client', async () => {
const alternateProxy: ModelAccessorBusClient = {
onAccessorChanged: sandbox.stub(),
closeSubscription: sandbox.stub(),
};
modelAccessorBusServer.setClient(alternateProxy);
expect(modelAccessorBusServer.getClient()).to.be.equal(alternateProxy);
});
});
describe('setClient', async () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('expect undefined', async () => {
modelAccessorBusServer.setClient(undefined);
expect(modelAccessorBusServer.getClient()).to.be.undefined;
});
it('expect same client', async () => {
modelAccessorBusServer.setClient(clientProxy);
expect(modelAccessorBusServer.getClient()).to.be.equal(clientProxy);
});
it('expect client change with disposeSubscriptions', async () => {
const alternateProxy: ModelAccessorBusClient = {
onAccessorChanged: sandbox.stub(),
closeSubscription: sandbox.stub(),
};
expect(
await modelAccessorBusServer.subscribe(appContext, 'test')
).to.be.eql(
{
id: 1,
accessorId: 'test',
},
'Subscribe did not provide the expected result.'
);
modelAccessorBusServer.setClient(alternateProxy);
expect(modelAccessorBusServer.getClient()).to.be.equal(
alternateProxy,
'Client does not match expected result'
);
});
});
describe('subscribe', async () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('no client', async () => {
modelAccessorBusServer.setClient(undefined);
expect(
await modelAccessorBusServer.subscribe(appContext, 'test')
).to.be.eql({
id: 1,
accessorId: 'test',
});
});
it('expect subscription token', async () => {
expect(
await modelAccessorBusServer.subscribe(appContext, 'test')
).to.be.eql({
id: 1,
accessorId: 'test',
});
});
it('expect 2 different subscription token', async () => {
expect(
await modelAccessorBusServer.subscribe(appContext, 'test1')
).to.be.eql({
id: 1,
accessorId: 'test1',
});
expect(
await modelAccessorBusServer.subscribe(appContext, 'test2')
).to.be.eql({
id: 2,
accessorId: 'test2',
});
});
});
describe('closeSubscription', async () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('execute closeSubscription with no subscription', async () => {
expect(() =>
modelAccessorBusServer.closeSubscription({
id: 1,
accessorId: 'test',
})
).not.to.throw();
});
it('execute closeSubscription', async () => {
const token = await modelAccessorBusServer.subscribe(appContext, 'test');
expect(token).to.be.eql(
{
id: 1,
accessorId: 'test',
},
'Subscribe did not provide the expected result.'
);
expect(() =>
modelAccessorBusServer.closeSubscription(token)
).not.to.throw();
});
it('execute dispose', async () => {
const token = await modelAccessorBusServer.subscribe(appContext, 'test');
expect(token).to.be.eql(
{
id: 1,
accessorId: 'test',
},
'Subscribe did not provide the expected result.'
);
modelAccessorBusServer.dispose();
});
});
describe('get', async () => {
it('expect undefined : existing context, not existing accessor', async () => {
expect(await modelAccessorBusServer.get(appContext, 'empty')).to.be
.undefined;
});
});
describe('trigger subscription', () => {
it('expect subscription token', async () => {
const providerChangeSubToken = await modelAccessorBusServer.subscribe(
appContext,
'fake-provider.get'
);
const fakeService = fakeContribution.getModelService<FakeModelService>();
expect(providerChangeSubToken).to.be.eql(
{
id: 1,
accessorId: 'fake-provider.get',
},
'Subscription did not provide the expected result.'
);
await fakeService.setName('Fake Model 2');
expect(clientProxy.onAccessorChanged.calledOnceWith(1)).to.be.true;
});
});
describe('getModelAccessorBus', async () => {
it('expect to be different on different contexts', async () => {
const spiedAccessorBusAccess = sandbox.spy(
modelAccessorBusServer as FullModelAccessorBusServer
);
await Promise.all([
modelAccessorBusServer.get(appContext, 'empty'),
modelAccessorBusServer.get(`${appContext}-2`, 'empty'),
]);
expect(spiedAccessorBusAccess.getModelAccessorBus.calledTwice).to.be.true;
const bus_0 = await spiedAccessorBusAccess.getModelAccessorBus
.returnValues[0];
const bus_1 = await spiedAccessorBusAccess.getModelAccessorBus
.returnValues[1];
expect(bus_0 !== bus_1).to.be.true;
});
it('expect to be same for same contexts', async () => {
const spiedAccessorBusAccess = sandbox.spy(
modelAccessorBusServer as FullModelAccessorBusServer
);
await Promise.all([
modelAccessorBusServer.get(appContext, 'empty'),
modelAccessorBusServer.get(appContext, 'empty'),
]);
expect(spiedAccessorBusAccess.getModelAccessorBus.calledTwice).to.be.true;
const bus_0 = await spiedAccessorBusAccess.getModelAccessorBus
.returnValues[0];
const bus_1 = await spiedAccessorBusAccess.getModelAccessorBus
.returnValues[1];
expect(bus_0 === bus_1).to.be.true;
});
});
});
export class MockProvider extends HubAwareProvider {
constructor() {
super('fake-provider');
this.getId = this.getId.bind(this);
this.accessors.set('get', this.getId);
}
setModelHub(modelHub: ModelHub<string, object>): void {
super.setModelHub(modelHub);
const subscription = modelHub.subscribe(FAKE_MODEL_ID);
subscription.onModelChanged = (
_modelId: string,
_model: object,
delta?: Operation[]
) => {
if (delta?.some((op) => op.path === '/name')) {
this.notify('get');
}
};
}
getId() {
return this.id;
}
}
const mockProvider = new MockProvider();
interface FakeModelService {
getModel(): FakeModel;
setName(newName: string): Promise<void>;
}
class FakeContribution extends AbstractModelServiceContribution<string> {
protected commandStack: CommandStack;
protected readonly model: FakeModel = {
name: 'Fake Model 1',
};
protected readonly modelService: FakeModelService = {
getModel: () => {
let model = this.modelManager.getModel<FakeModel>(FAKE_MODEL_ID);
if (!model) {
this.modelManager.setModel(FAKE_MODEL_ID, this.model);
model = this.modelManager.getModel<FakeModel>(FAKE_MODEL_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(FAKE_MODEL_ID, newName);
await this.commandStack.execute(command);
},
};
getModelService<S>(): S {
return this.modelService as S;
}
setModelManager(modelManager: ModelManager<string>): void {
super.setModelManager(modelManager);
this.commandStack = modelManager.getCommandStack(FAKE_MODEL_ID);
}
constructor() {
super();
this.initialize({
id: 'test.contrib1',
persistenceContribution: {
canHandle: (modelId) => Promise.resolve(modelId === FAKE_MODEL_ID),
loadModel: (modelId) =>
modelId === FAKE_MODEL_ID
? Promise.resolve(this.model)
: Promise.reject(new Error(`No such model: ${modelId}`)),
saveModel: async (modelId, model) => {
if (modelId !== FAKE_MODEL_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;
},
},
modelAccessorContribution: {
getProviders() {
return [mockProvider];
},
},
});
}
}
function createSetNameCommand(
modelId: string,
newName: string
): SimpleCommand<string> {
return createModelUpdaterCommand<string, FakeModel>(
'Set Name',
modelId,
(model) => {
model.name = newName;
}
);
}