UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

251 lines (194 loc) 7.66 kB
// ***************************************************************************** // 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 chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ExclusiveExecutor } from '../promise-util'; chai.use(chaiAsPromised); const TEST_CONTEXT = 'test-context'; describe('ExclusiveExecutor', () => { let exec: ExclusiveExecutor<string>; beforeEach(() => { exec = new 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]); expect(a).to.eventually.be.equal(1); expect(b).to.eventually.be.equal(2); 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]); expect(a).to.eventually.be.equal(1); expect(b).to.eventually.be.rejected; 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]); expect(a).to.eventually.be.rejectedWith('Boom!A'); 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'; } expect(aOK).to.be.true; expect(bOK).to.be.true; 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(); expect(operation1.state).to.equal('running'); expect(operation2.state).to.equal('ready'); expect(operation3.state).to.equal('running'); await operation1.complete(); expect(operation1.state).to.equal('complete'); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('running'); await operation3.complete(); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('complete'); await operation2.complete(); 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(); expect(operation1.state).to.equal('running'); expect(operation2.state).to.equal('ready'); expect(operation3.state).to.equal('running'); await operation1.complete(); expect(operation1.state).to.equal('complete'); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('running'); await operation3.complete(); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('complete'); await operation2.complete(); 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(); expect(operation1.state).to.equal('running'); expect(operation2.state).to.equal('ready'); expect(operation3.state).to.equal('ready'); await operation1.complete(); expect(operation1.state).to.equal('complete'); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('running'); await operation3.complete(); expect(operation2.state).to.equal('running'); expect(operation3.state).to.equal('complete'); await operation2.complete(); 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]); expect(a).to.eventually.be.equal(1); expect(b).to.eventually.be.rejected; expect(c).to.eventually.be.equal(2); }); }); }); class MockOperation { private resolve: (value: unknown) => void; private result: Promise<unknown> = new Promise<unknown>((resolve) => { this.resolve = resolve; }); public state: OperationState = 'ready'; async run(): Promise<unknown> { this.state = 'running'; return this.result; } async complete(value?: unknown): Promise<void> { if (this.state !== 'running') { throw new Error('Unexpected state: ' + this.state); } this.resolve(value); this.state = 'complete'; await flush(); } } type OperationState = 'ready' | 'running' | 'complete'; /** * 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)); }