@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
226 lines • 9.94 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 chai_1 = __importStar(require("chai"));
const chai_as_promised_1 = __importDefault(require("chai-as-promised"));
const promise_util_1 = require("../promise-util");
chai_1.default.use(chai_as_promised_1.default);
const TEST_CONTEXT = 'test-context';
describe('ExclusiveExecutor', () => {
let exec;
beforeEach(() => {
exec = new promise_util_1.ExclusiveExecutor();
});
it('executes in the order posted', async () => {
let x = 0;
const a = exec.run(async () => ++x, [TEST_CONTEXT]);
const b = exec.run(async () => ++x, [TEST_CONTEXT]);
const c = exec.run(async () => ++x, [TEST_CONTEXT]);
(0, chai_1.expect)(a).to.eventually.be.equal(1);
(0, chai_1.expect)(b).to.eventually.be.equal(2);
(0, chai_1.expect)(c).to.eventually.be.equal(3);
});
it('does not leak exceptions', async () => {
let x = 0;
const a = exec.run(async () => ++x, [TEST_CONTEXT]);
const b = exec.run(async () => {
throw new Error();
}, [TEST_CONTEXT]);
const c = exec.run(async () => ++x, [TEST_CONTEXT]);
(0, chai_1.expect)(a).to.eventually.be.equal(1);
(0, chai_1.expect)(b).to.eventually.be.rejected;
(0, chai_1.expect)(c).to.eventually.be.equal(2);
});
it('gives each caller its exception', async () => {
const a = exec.run(async () => {
throw new Error('Boom!A');
}, [TEST_CONTEXT]);
const b = exec.run(async () => {
throw new Error('Boom!B');
}, [TEST_CONTEXT]);
(0, chai_1.expect)(a).to.eventually.be.rejectedWith('Boom!A');
(0, chai_1.expect)(b).to.eventually.be.rejectedWith('Boom!B');
});
it('try/catch usage is predictable', async () => {
let aOK = false;
let bOK = false;
let cOK = false;
try {
await exec.run(async () => {
throw new Error('Boom!A');
}, [TEST_CONTEXT]);
}
catch (error) {
aOK = error.message === 'Boom!A';
}
try {
const b = await exec.run(async () => 'b'.toUpperCase(), [TEST_CONTEXT]);
bOK = b === 'B';
}
catch (error) {
bOK = false;
}
try {
await exec.run(async () => {
throw new Error('Boom!C');
}, [TEST_CONTEXT]);
}
catch (error) {
cOK = error.message === 'Boom!C';
}
(0, chai_1.expect)(aOK).to.be.true;
(0, chai_1.expect)(bOK).to.be.true;
(0, chai_1.expect)(cOK).to.be.true;
});
it('waits for context dependencies', async () => {
const operation1 = new MockOperation();
const operation2 = new MockOperation();
const operation3 = new MockOperation();
// Operation 1 and 3 are completely independent and can run in parallel
// Operation 2 depends on Operation 1 because they use the same context
exec.run(() => operation1.run(), [TEST_CONTEXT], []);
exec.run(() => operation2.run(), [TEST_CONTEXT], []);
exec.run(() => operation3.run(), ['context2'], []);
await flush();
(0, chai_1.expect)(operation1.state).to.equal('running');
(0, chai_1.expect)(operation2.state).to.equal('ready');
(0, chai_1.expect)(operation3.state).to.equal('running');
await operation1.complete();
(0, chai_1.expect)(operation1.state).to.equal('complete');
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('running');
await operation3.complete();
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('complete');
await operation2.complete();
(0, chai_1.expect)(operation2.state).to.equal('complete');
});
it('waits for model dependencies', async () => {
const operation1 = new MockOperation();
const operation2 = new MockOperation();
const operation3 = new MockOperation();
// Operation 1 and 3 are completely independent and can run in parallel
// Operation 2 depends on Operation 1 because they use the same model
exec.run(() => operation1.run(), [TEST_CONTEXT], ['modelA']);
exec.run(() => operation2.run(), ['context2'], ['modelC', 'modelA']);
exec.run(() => operation3.run(), ['context3'], ['modelB']);
await flush();
(0, chai_1.expect)(operation1.state).to.equal('running');
(0, chai_1.expect)(operation2.state).to.equal('ready');
(0, chai_1.expect)(operation3.state).to.equal('running');
await operation1.complete();
(0, chai_1.expect)(operation1.state).to.equal('complete');
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('running');
await operation3.complete();
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('complete');
await operation2.complete();
(0, chai_1.expect)(operation2.state).to.equal('complete');
});
it('blocks any other operation', async () => {
const operation1 = new MockOperation();
const operation2 = new MockOperation();
const operation3 = new MockOperation();
// Operation 1 blocks everything else (e.g. Undo/Redo operation with no model id)
// Operation 2 and Operation 3 are independent from each other and can run in parallel
exec.run(() => operation1.run(), [TEST_CONTEXT]);
exec.run(() => operation2.run(), ['context2'], ['modelC', 'modelA']);
exec.run(() => operation3.run(), ['context3'], ['modelB', 'modelD']);
await flush();
(0, chai_1.expect)(operation1.state).to.equal('running');
(0, chai_1.expect)(operation2.state).to.equal('ready');
(0, chai_1.expect)(operation3.state).to.equal('ready');
await operation1.complete();
(0, chai_1.expect)(operation1.state).to.equal('complete');
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('running');
await operation3.complete();
(0, chai_1.expect)(operation2.state).to.equal('running');
(0, chai_1.expect)(operation3.state).to.equal('complete');
await operation2.complete();
(0, chai_1.expect)(operation2.state).to.equal('complete');
});
describe('Corner cases', () => {
it('sequencing lock handles non-errors thrown', async () => {
let x = 0;
const a = exec.run(async () => ++x, [TEST_CONTEXT]);
const b = exec.run(async () => {
throw '💣';
}, [TEST_CONTEXT]);
const c = exec.run(async () => ++x, [TEST_CONTEXT]);
(0, chai_1.expect)(a).to.eventually.be.equal(1);
(0, chai_1.expect)(b).to.eventually.be.rejected;
(0, chai_1.expect)(c).to.eventually.be.equal(2);
});
});
});
class MockOperation {
constructor() {
this.result = new Promise((resolve) => {
this.resolve = resolve;
});
this.state = 'ready';
}
async run() {
this.state = 'running';
return this.result;
}
async complete(value) {
if (this.state !== 'running') {
throw new Error('Unexpected state: ' + this.state);
}
this.resolve(value);
this.state = 'complete';
await flush();
}
}
/**
* Process the async queue. This is necessary to ensure then() is executed
* as soon as possible after a promise completes. This way, we can
* ensure that dependencies of operations are started before we run our
* test assertions.
*/
async function flush() {
await new Promise((resolve) => setTimeout(resolve));
}
//# sourceMappingURL=promise-util.spec.js.map