UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

226 lines 9.94 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 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