UNPKG

@itwin/core-backend

Version:
409 lines • 23.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { DbConflictResolution, Guid } from "@itwin/core-bentley"; import { IModel, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import { assert } from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { HubWrappers, KnownTestLocations } from ".."; import { ChannelControl, IModelHost, SpatialCategory, SqliteChangesetReader } from "../../core-backend"; import { HubMock } from "../../internal/HubMock"; import { IModelTestUtils, TestUserType } from "../IModelTestUtils"; chai.use(chaiAsPromised); async function assertThrowsAsync(test, msg) { try { await test(); } catch (e) { if (e instanceof Error && msg) { assert.equal(e.message, msg); } return; } throw new Error(`Failed to throw error with message: "${msg}"`); } async function updatePhysicalObject(b, el1, federationGuid) { await b.locks.acquireLocks({ exclusive: el1 }); const props = b.elements.getElement(el1); props.federationGuid = federationGuid; b.elements.updateElement(props.toJSON()); } describe("Change merge method", () => { const ctx = { accessTokens: { user1: "", user2: "", user3: "", }, iModelId: "", iTwinId: "", modelId: "", spatialCategoryId: "", iModelName: "TestIModel", rootSubject: "TestSubject", openBriefcase: async (user, noLock) => { const b = await HubWrappers.downloadAndOpenBriefcase({ accessToken: ctx.accessTokens[user], iTwinId: ctx.iTwinId, iModelId: ctx.iModelId, noLock }); b.channels.addAllowedChannel(ChannelControl.sharedChannelName); return b; }, openB1: async (noLock) => { return ctx.openBriefcase("user1", noLock); }, openB2: async (noLock) => { return ctx.openBriefcase("user2", noLock); }, openB3: async (noLock) => { return ctx.openBriefcase("user3", noLock); }, }; async function insertPhysicalObject(b) { await b.locks.acquireLocks({ shared: ctx.modelId }); return b.elements.insertElement(IModelTestUtils.createPhysicalObject(b, ctx.modelId, ctx.spatialCategoryId).toJSON()); } before(async () => { await IModelHost.startup(); HubMock.startup("PullMergeMethod", KnownTestLocations.outputDir); }); after(async () => { HubMock.shutdown(); //await IModelHost.shutdown(); }); beforeEach(async () => { ctx.iTwinId = HubMock.iTwinId; ctx.accessTokens.user1 = await HubWrappers.getAccessToken(TestUserType.SuperManager); ctx.accessTokens.user2 = await HubWrappers.getAccessToken(TestUserType.Regular); ctx.accessTokens.user3 = await HubWrappers.getAccessToken(TestUserType.Super); ctx.iModelId = await HubMock.createNewIModel({ accessToken: ctx.accessTokens.user1, iTwinId: ctx.iTwinId, iModelName: ctx.iModelName, description: ctx.rootSubject }); assert.isNotEmpty(ctx.iModelId); const b1 = await ctx.openB1(); await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); [, ctx.modelId] = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(b1, IModelTestUtils.getUniqueModelCode(b1, "newPhysicalModel"), true); const dictionary = b1.models.getModel(IModel.dictionaryId); const newCategoryCode = IModelTestUtils.getUniqueSpatialCategoryCode(dictionary, "ThisTestSpatialCategory"); ctx.spatialCategoryId = SpatialCategory.insert(dictionary.iModel, dictionary.id, newCategoryCode.value, new SubCategoryAppearance({ color: 0xff0000 })); b1.saveChanges(); await b1.pushChanges({ description: "" }); b1.close(); }); it("rebase events (noFastForward:true)", async () => { /** * Fastforward will not trigger rebase events as rebase was not required to merge changes. * In this test we will test rebase events when noFastForward is set to true. Which mean rebase is required to merge changes. */ const events = new Map(); const b1 = await ctx.openB1(); events.set(b1.briefcaseId, []); b1.txns.onRebaseTxnBegin.addListener((args) => { events.get(b1.briefcaseId)?.push({ args, event: "onRebaseTxnBegin" }); }); b1.txns.onRebaseTxnEnd.addListener((args) => { events.get(b1.briefcaseId)?.push({ args, event: "onRebaseTxnEnd" }); }); const b2 = await ctx.openB2(); events.set(b2.briefcaseId, []); b2.txns.onRebaseTxnBegin.addListener((args) => { events.get(b2.briefcaseId)?.push({ args, event: "onRebaseTxnBegin" }); }); b2.txns.onRebaseTxnEnd.addListener((args) => { events.get(b2.briefcaseId)?.push({ args, event: "onRebaseTxnEnd" }); }); const e1 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e1}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e1}]` }); assert.isDefined(b1.elements.getElement(e1)); assert.equal(events.get(b1.briefcaseId)?.length, 0); const e2 = await insertPhysicalObject(b1); b1.saveChanges(); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e2}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 0); assert.isDefined(b1.elements.getElement(e2)); const e3 = await insertPhysicalObject(b2); b2.saveChanges(`inserted physical object [id=${e3}]`); const e4 = await insertPhysicalObject(b2); b2.saveChanges(`inserted physical object [id=${e4}]`); events.set(b2.briefcaseId, []); // fast-forward await b2.pushChanges({ description: `inserted physical object [id=${e3},${e4}]`, noFastForward: true }); assert.equal(events.get(b2.briefcaseId)?.length, 4); assert.equal(events.get(b2.briefcaseId)?.[0].event, "onRebaseTxnBegin"); assert.equal(events.get(b2.briefcaseId)?.[0].args.id, "0x100000000"); assert.equal(events.get(b2.briefcaseId)?.[0].args.descr, "inserted physical object [id=0x40000000001]"); assert.equal(events.get(b2.briefcaseId)?.[0].args.type, "Data"); assert.equal(events.get(b2.briefcaseId)?.[3].event, "onRebaseTxnEnd"); assert.equal(events.get(b2.briefcaseId)?.[3].args.id, "0x100000001"); assert.equal(events.get(b2.briefcaseId)?.[3].args.descr, "inserted physical object [id=0x40000000002]"); assert.equal(events.get(b2.briefcaseId)?.[3].args.type, "Data"); assert.isDefined(b2.elements.getElement(e1)); assert.isDefined(b2.elements.getElement(e2)); assert.isDefined(b2.elements.getElement(e3)); assert.isDefined(b2.elements.getElement(e4)); const e5 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e5}]`); const e6 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e6}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e5}, ${e6}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 4); assert.equal(events.get(b1.briefcaseId)?.[0].event, "onRebaseTxnBegin"); assert.equal(events.get(b1.briefcaseId)?.[0].args.id, "0x100000000"); assert.equal(events.get(b1.briefcaseId)?.[0].args.descr, "inserted physical object [id=0x30000000003]"); assert.equal(events.get(b1.briefcaseId)?.[0].args.type, "Data"); assert.equal(events.get(b1.briefcaseId)?.[3].event, "onRebaseTxnEnd"); assert.equal(events.get(b1.briefcaseId)?.[3].args.id, "0x100000001"); assert.equal(events.get(b1.briefcaseId)?.[3].args.descr, "inserted physical object [id=0x30000000004]"); assert.equal(events.get(b1.briefcaseId)?.[3].args.type, "Data"); assert.isDefined(b1.elements.getElement(e1)); assert.isDefined(b1.elements.getElement(e2)); assert.isDefined(b1.elements.getElement(e3)); // Not found assert.isDefined(b1.elements.getElement(e4)); assert.isDefined(b1.elements.getElement(e5)); assert.isDefined(b1.elements.getElement(e6)); events.set(b2.briefcaseId, []); await b2.pullChanges(); assert.equal(events.get(b2.briefcaseId)?.length, 0); assert.isDefined(b2.elements.getElement(e1)); assert.isDefined(b2.elements.getElement(e2)); assert.isDefined(b2.elements.getElement(e3)); assert.isDefined(b2.elements.getElement(e4)); assert.isDefined(b2.elements.getElement(e5)); assert.isDefined(b2.elements.getElement(e6)); await updatePhysicalObject(b1, e3, Guid.createValue()); b1.saveChanges(`update physical object [id=${e3}]`); await updatePhysicalObject(b1, e4, Guid.createValue()); b1.saveChanges(`update physical object [id=${e4}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `update physical object [id=${e3},${e4}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 0); await updatePhysicalObject(b2, e1, Guid.createValue()); b2.saveChanges(`update physical object [id=${e1}]`); await updatePhysicalObject(b2, e2, Guid.createValue()); b2.saveChanges(`update physical object [id=${e2}]`); await updatePhysicalObject(b2, e5, Guid.createValue()); b2.saveChanges(`update physical object [id=${e5}]`); await updatePhysicalObject(b2, e6, Guid.createValue()); b2.saveChanges(`update physical object [id=${e6}]`); events.set(b2.briefcaseId, []); await b2.pushChanges({ description: `update physical object [id=${e1},${e2},${e5}]`, noFastForward: true }); assert.equal(events.get(b2.briefcaseId)?.length, 8); assert.equal(events.get(b2.briefcaseId)?.[0].event, "onRebaseTxnBegin"); assert.equal(events.get(b2.briefcaseId)?.[0].args.id, "0x100000000"); assert.equal(events.get(b2.briefcaseId)?.[0].args.descr, "update physical object [id=0x30000000001]"); assert.equal(events.get(b2.briefcaseId)?.[0].args.type, "Data"); assert.equal(events.get(b2.briefcaseId)?.[2].event, "onRebaseTxnBegin"); assert.equal(events.get(b2.briefcaseId)?.[2].args.id, "0x100000001"); assert.equal(events.get(b2.briefcaseId)?.[2].args.descr, "update physical object [id=0x30000000002]"); assert.equal(events.get(b2.briefcaseId)?.[2].args.type, "Data"); assert.equal(events.get(b2.briefcaseId)?.[4].event, "onRebaseTxnBegin"); assert.equal(events.get(b2.briefcaseId)?.[4].args.id, "0x100000002"); assert.equal(events.get(b2.briefcaseId)?.[4].args.descr, "update physical object [id=0x30000000003]"); assert.equal(events.get(b2.briefcaseId)?.[4].args.type, "Data"); assert.equal(events.get(b2.briefcaseId)?.[6].event, "onRebaseTxnBegin"); assert.equal(events.get(b2.briefcaseId)?.[6].args.id, "0x100000003"); assert.equal(events.get(b2.briefcaseId)?.[6].args.descr, "update physical object [id=0x30000000004]"); assert.equal(events.get(b2.briefcaseId)?.[6].args.type, "Data"); assert.isDefined(b1.elements.getElement(e1).federationGuid); assert.isDefined(b1.elements.getElement(e2).federationGuid); assert.isDefined(b1.elements.getElement(e3).federationGuid); assert.isDefined(b1.elements.getElement(e4).federationGuid); assert.isDefined(b1.elements.getElement(e5).federationGuid); assert.isDefined(b1.elements.getElement(e6).federationGuid); assert.isDefined(b2.elements.getElement(e1).federationGuid); assert.isDefined(b2.elements.getElement(e2).federationGuid); assert.isDefined(b2.elements.getElement(e3).federationGuid); assert.isDefined(b2.elements.getElement(e4).federationGuid); assert.isDefined(b2.elements.getElement(e5).federationGuid); assert.isDefined(b2.elements.getElement(e6).federationGuid); b1.close(); b2.close(); }); it("rebase events (noFastForward:false/default)", async () => { /** * Fastforward will not trigger rebase events as rebase was not required to merge changes. * In this test we will test rebase events when noFastForward is set to false. Which mean rebase is not required to merge changes. */ const events = new Map(); const b1 = await ctx.openB1(); events.set(b1.briefcaseId, []); b1.txns.onRebaseTxnBegin.addListener((args) => { events.get(b1.briefcaseId)?.push({ args, event: "onRebaseTxnBegin" }); }); b1.txns.onRebaseTxnEnd.addListener((args) => { events.get(b1.briefcaseId)?.push({ args, event: "onRebaseTxnEnd" }); }); const b2 = await ctx.openB2(); events.set(b2.briefcaseId, []); b2.txns.onRebaseTxnBegin.addListener((args) => { events.get(b2.briefcaseId)?.push({ args, event: "onRebaseTxnBegin" }); }); b2.txns.onRebaseTxnEnd.addListener((args) => { events.get(b2.briefcaseId)?.push({ args, event: "onRebaseTxnEnd" }); }); const e1 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e1}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e1}]` }); assert.isDefined(b1.elements.getElement(e1)); assert.equal(events.get(b1.briefcaseId)?.length, 0); const e2 = await insertPhysicalObject(b1); b1.saveChanges(); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e2}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 0); assert.isDefined(b1.elements.getElement(e2)); const e3 = await insertPhysicalObject(b2); b2.saveChanges(`inserted physical object [id=${e3}]`); const e4 = await insertPhysicalObject(b2); b2.saveChanges(`inserted physical object [id=${e4}]`); events.set(b2.briefcaseId, []); // fast-forward await b2.pushChanges({ description: `inserted physical object [id=${e3},${e4}]` }); assert.equal(events.get(b2.briefcaseId)?.length, 0); assert.isDefined(b2.elements.getElement(e1)); assert.isDefined(b2.elements.getElement(e2)); assert.isDefined(b2.elements.getElement(e3)); assert.isDefined(b2.elements.getElement(e4)); const e5 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e5}]`); const e6 = await insertPhysicalObject(b1); b1.saveChanges(`inserted physical object [id=${e6}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `inserted physical object [id=${e5}, ${e6}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 0); assert.isDefined(b1.elements.getElement(e1)); assert.isDefined(b1.elements.getElement(e2)); assert.isDefined(b1.elements.getElement(e3)); // Not found assert.isDefined(b1.elements.getElement(e4)); assert.isDefined(b1.elements.getElement(e5)); assert.isDefined(b1.elements.getElement(e6)); events.set(b2.briefcaseId, []); await b2.pullChanges(); assert.equal(events.get(b2.briefcaseId)?.length, 0); assert.isDefined(b2.elements.getElement(e1)); assert.isDefined(b2.elements.getElement(e2)); assert.isDefined(b2.elements.getElement(e3)); assert.isDefined(b2.elements.getElement(e4)); assert.isDefined(b2.elements.getElement(e5)); assert.isDefined(b2.elements.getElement(e6)); await updatePhysicalObject(b1, e3, Guid.createValue()); b1.saveChanges(`update physical object [id=${e3}]`); await updatePhysicalObject(b1, e4, Guid.createValue()); b1.saveChanges(`update physical object [id=${e4}]`); events.set(b1.briefcaseId, []); await b1.pushChanges({ description: `update physical object [id=${e3},${e4}]` }); assert.equal(events.get(b1.briefcaseId)?.length, 0); await updatePhysicalObject(b2, e1, Guid.createValue()); b2.saveChanges(`update physical object [id=${e1}]`); await updatePhysicalObject(b2, e2, Guid.createValue()); b2.saveChanges(`update physical object [id=${e2}]`); await updatePhysicalObject(b2, e5, Guid.createValue()); b2.saveChanges(`update physical object [id=${e5}]`); await updatePhysicalObject(b2, e6, Guid.createValue()); b2.saveChanges(`update physical object [id=${e6}]`); events.set(b2.briefcaseId, []); await b2.pushChanges({ description: `update physical object [id=${e1},${e2},${e5}]` }); assert.isDefined(b1.elements.getElement(e1).federationGuid); assert.isDefined(b1.elements.getElement(e2).federationGuid); assert.isDefined(b1.elements.getElement(e3).federationGuid); assert.isDefined(b1.elements.getElement(e4).federationGuid); assert.isDefined(b1.elements.getElement(e5).federationGuid); assert.isDefined(b1.elements.getElement(e6).federationGuid); assert.isDefined(b2.elements.getElement(e1).federationGuid); assert.isDefined(b2.elements.getElement(e2).federationGuid); assert.isDefined(b2.elements.getElement(e3).federationGuid); assert.isDefined(b2.elements.getElement(e4).federationGuid); assert.isDefined(b2.elements.getElement(e5).federationGuid); assert.isDefined(b2.elements.getElement(e6).federationGuid); b1.close(); b2.close(); }); it("rebase with be_props (insert conflict)", async () => { const b1 = await ctx.openB1(); const b2 = await ctx.openB2(); b1.saveFileProperty({ namespace: "test", name: "test" }, "test1"); b1.saveChanges("test"); await b1.pushChanges({ description: "test" }); b2.saveFileProperty({ namespace: "test", name: "test" }, "test2"); b2.saveChanges("test2"); await assertThrowsAsync(async () => b2.pushChanges({ description: "test2" }), "PRIMARY KEY insert conflict. Aborting rebase."); assert.equal(b2.queryFilePropertyString({ namespace: "test", name: "test" }), "test1"); b2.saveFileProperty({ namespace: "test", name: "test" }, "test3"); chai.expect(() => b2.saveChanges("test1")).throws("Could not save changes (test1)"); b2.abandonChanges(); // set handler to resolve conflict b2.txns.changeMergeManager.addConflictHandler({ id: "my", handler: (args) => { if (args.cause === "Conflict") { if (args.tableName === "be_Prop") { if (args.opcode === "Inserted") { chai.expect(args.getColumnNames()).to.be.deep.equal(["Namespace", "Name", "Id", "SubId", "TxnMode", "StrData", "RawSize", "Data"]); chai.expect(args.txn.id).to.be.equal("0x100000000"); chai.expect(args.txn.descr).to.be.equal("test2"); chai.expect(args.txn.type).to.be.equal("Data"); const localChangedVal = args.getValueText(5, "New"); const tipValue = b2.queryFilePropertyString({ namespace: "test", name: "test" }); b2.saveFileProperty({ namespace: "test", name: "test" }, `${tipValue} + ${localChangedVal}`); return DbConflictResolution.Skip; // skip incomming value and continue } } } return undefined; } }); // resume rebase see if it resolve the conflict b2.txns.changeMergeManager.resume(); // use changeset api to read txn directly const reader = SqliteChangesetReader.openTxn({ db: b2, txnId: "0x100000000" }); chai.expect(reader.step()).to.be.true; chai.expect(reader.tableName).to.be.equal("be_Prop"); chai.expect(reader.getColumnNames(reader.tableName)[5]).to.be.equal("StrData"); // note the operation changed from insert to update chai.expect(reader.op).to.be.equal("Updated"); // note this old value is from master branch after changeset was recomputed chai.expect(reader.getChangeValueText(5, "Old")).to.be.equal("test1"); // note this new value is from local branch after merger. chai.expect(reader.getChangeValueText(5, "New")).to.be.equal("test1 + test2"); reader.close(); assert.equal(b2.queryFilePropertyString({ namespace: "test", name: "test" }), "test1 + test2"); await b2.pushChanges({ description: "test2" }); await b1.pullChanges(); assert.equal(b2.queryFilePropertyString({ namespace: "test", name: "test" }), "test1 + test2"); assert.equal(b1.queryFilePropertyString({ namespace: "test", name: "test" }), "test1 + test2"); b1.close(); b2.close(); }); it("rebase with be_props (data conflict) ", async () => { const b1 = await ctx.openB1(); const b2 = await ctx.openB2(); b1.saveFileProperty({ namespace: "test", name: "test" }, "test1"); b1.saveChanges("test"); await b1.pushChanges({ description: "test" }); await b2.pullChanges(); b2.saveFileProperty({ namespace: "test", name: "test" }, "test2"); b2.saveChanges("test2"); b1.saveFileProperty({ namespace: "test", name: "test" }, "test3"); b1.saveChanges("test"); await b1.pushChanges({ description: "test" }); // set handler to resolve conflict b2.txns.changeMergeManager.addConflictHandler({ id: "my", handler: (args) => { if (args.cause === "Data") { if (args.tableName === "be_Prop") { if (args.opcode === "Updated") { const localChangedVal = args.getValueText(5, "New"); const tipValue = b2.queryFilePropertyString({ namespace: "test", name: "test" }); b2.saveFileProperty({ namespace: "test", name: "test" }, `${tipValue} + ${localChangedVal}`); return DbConflictResolution.Skip; // skip incomming value and continue } } } return undefined; } }); await b2.pushChanges({ description: "test" }); await b2.pullChanges(); await b1.pullChanges(); assert.equal(b2.queryFilePropertyString({ namespace: "test", name: "test" }), "test3 + test2"); assert.equal(b1.queryFilePropertyString({ namespace: "test", name: "test" }), "test3 + test2"); b1.close(); b2.close(); }); }); //# sourceMappingURL=ChangeMerge.test.js.map