UNPKG

@itwin/core-backend

Version:
909 lines • 108 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { DbResult, Guid } from "@itwin/core-bentley"; import { Code, GeometryStreamBuilder, IModel, QueryBinder, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { HubWrappers, IModelTestUtils, KnownTestLocations } from ".."; import { _nativeDb, BriefcaseManager, ChangesetECAdaptor, ChannelControl, DrawingCategory, ElementGroupsMembers, SqliteChangesetReader } from "../../core-backend"; import { HubMock } from "../../internal/HubMock"; import { StashManager } from "../../StashManager"; import { EditTxn } from "../../EditTxn"; import { TestUtils } from "../TestUtils"; import { existsSync, unlinkSync, writeFileSync } from "fs"; import * as path from "path"; import { LineSegment3d, Point3d } from "@itwin/core-geometry"; chai.use(chaiAsPromised); function startTestTxn(iModel, description = "rebase") { const txn = new EditTxn(iModel, description); txn.start(); return txn; } async function importSchemaStrings(txn, schemas) { if (txn.isActive) txn.saveChanges(); await txn.iModel.importSchemaStrings(schemas); } class TestIModel { iModelId = ""; drawingModelId = ""; drawingCategoryId = ""; briefcases = []; _data = 0; _nextAccessToken = 1; constructor() { } acquireAccessToken() { return `user${this._nextAccessToken++}`; } async startup() { HubMock.startup("TestIModel", KnownTestLocations.outputDir); const accessToken = this.acquireAccessToken(); this.iModelId = await HubMock.createNewIModel({ accessToken, iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" }); const b1 = await HubWrappers.downloadAndOpenBriefcase({ accessToken, iTwinId: HubMock.iTwinId, iModelId: this.iModelId }); const b1Txn = startTestTxn(b1, "rebase startup"); b1.channels.addAllowedChannel(ChannelControl.sharedChannelName); const schema1 = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="ts" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="a1"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="prop1" typeName="string" /> </ECEntityClass> <ECEntityClass typeName="A1Recipe2d"> <BaseClass>bis:TemplateRecipe2d</BaseClass> <ECProperty propertyName="prop1" typeName="string" /> </ECEntityClass> <ECRelationshipClass typeName="A1OwnsA1" modifier="None" strength="embedding"> <BaseClass>bis:ElementOwnsChildElements</BaseClass> <Source multiplicity="(0..1)" roleLabel="owns" polymorphic="true"> <Class class="a1"/> </Source> <Target multiplicity="(0..*)" roleLabel="is owned by" polymorphic="false"> <Class class="a1"/> </Target> </ECRelationshipClass> </ECSchema>`; await importSchemaStrings(b1Txn, [schema1]); chai.expect(b1.txns.hasPendingTxns).to.be.true; await b1.pushChanges({ description: "schema1" }); const codeProps = Code.createEmpty(); codeProps.value = "DrawingModel"; await b1.locks.acquireLocks({ shared: IModel.dictionaryId }); this.drawingModelId = IModelTestUtils.createAndInsertDrawingPartitionAndModel(b1Txn, codeProps, true)[1]; let drawingCategoryId = DrawingCategory.queryCategoryIdByName(b1, IModel.dictionaryId, "MyDrawingCategory"); if (undefined === drawingCategoryId) drawingCategoryId = DrawingCategory.insert(b1Txn, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); this.drawingCategoryId = drawingCategoryId; b1Txn.saveChanges(); await b1.pushChanges({ description: "drawing category" }); b1.close(); } async openBriefcase() { const b = await HubWrappers.downloadAndOpenBriefcase({ accessToken: this.acquireAccessToken(), iTwinId: HubMock.iTwinId, iModelId: this.iModelId }); b.channels.addAllowedChannel(ChannelControl.sharedChannelName); b[_nativeDb].saveChanges(); this.briefcases.push(b); return b; } async insertRecipe2d(txn, markAsIndirect) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [IModel.dictionaryId] }); const baseProps = { classFullName: "TestDomain:A1Recipe2d", model: IModel.dictionaryId, code: Code.createEmpty(), }; let id = ""; if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { id = txn.insertElement({ ...baseProps, prop1: `${this._data++}` }); }); return id; } return txn.insertElement({ ...baseProps, prop1: `${this._data++}` }); } async updateRecipe2d(txn, id, markAsIndirect) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [IModel.dictionaryId], exclusive: [id] }); const elProps = b.elements.getElementProps(id); if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { txn.updateElement({ ...elProps, prop1: `${this._data++}` }); }); } else { txn.updateElement({ ...elProps, prop1: `${this._data++}` }); } } async insertElement(txn, markAsIndirect) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [this.drawingModelId] }); const builder = new GeometryStreamBuilder(); const p1 = Point3d.createZero(); const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 }); const circle = LineSegment3d.create(p1, p2); builder.appendGeometry(circle); const baseProps = { classFullName: "TestDomain:a1", model: this.drawingModelId, category: this.drawingCategoryId, code: Code.createEmpty(), geom: builder.geometryStream, prop1: `${this._data++}`, }; let id = ""; if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { id = txn.insertElement(baseProps); }); return id; } baseProps.prop1 = `${this._data++}`; return txn.insertElement(baseProps); } async insertElementEx(txn, args) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [this.drawingModelId] }); const builder = new GeometryStreamBuilder(); const p1 = Point3d.createZero(); const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 }); const circle = LineSegment3d.create(p1, p2); builder.appendGeometry(circle); const props = { classFullName: "TestDomain:a1", model: this.drawingModelId, category: this.drawingCategoryId, code: Code.createEmpty(), parent: args?.parent, geom: builder.geometryStream, prop1: args?.prop1 ?? `${this._data++}` }; let id = ""; if (args?.markAsIndirect) { b.txns.withIndirectTxnMode(() => { id = txn.insertElement(props); }); return id; } return txn.insertElement(props); } async updateElement(txn, id, markAsIndirect, updateGeom) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [this.drawingModelId], exclusive: [id] }); const elProps = b.elements.getElementProps(id); if (updateGeom) { const builder = new GeometryStreamBuilder(); const p1 = Point3d.createZero(); const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 10.0, y: 0.0, z: 0.0 }); const circle = LineSegment3d.create(p1, p2); builder.appendGeometry(circle); elProps.geom = builder.geometryStream; } if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { txn.updateElement({ ...elProps, prop1: `${this._data++}` }); }); } else { txn.updateElement({ ...elProps, prop1: `${this._data++}` }); } } async deleteElement(txn, id, markAsIndirect) { const b = txn.iModel; await b.locks.acquireLocks({ shared: [this.drawingModelId], exclusive: [id] }); if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { txn.deleteElement(id); }); } else { txn.deleteElement(id); } } async shutdown() { this.briefcases.forEach(b => b.close()); HubMock.shutdown(); } } const removePropertyRecursive = (obj, prop) => { if (obj && typeof obj === "object") { Object.keys(obj).forEach((key) => { if (key === prop) { delete obj[key]; } else { removePropertyRecursive(obj[key], prop); } }); } }; for (const enableSemanticRebase of [false, true]) { describe(`rebase changes & stashing api (useSemanticRebase=${enableSemanticRebase})`, function () { let testIModel; before(async () => { await TestUtils.shutdownBackend(); await TestUtils.startBackend({ useSemanticRebase: enableSemanticRebase }); }); after(async () => { await TestUtils.shutdownBackend(); await TestUtils.startBackend(); // restart normal backend so subsequent test suites aren't left without IModelHost }); this.beforeEach(async () => { testIModel = new TestIModel(); await testIModel.startup(); }); this.afterEach(async () => { await testIModel.shutdown(); }); it("save changes args", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "save changes args"); await testIModel.insertElement(b1Txn); b1Txn.saveChanges({ source: "test", description: "test description", appData: { test: "test", foo: [1, 2, 3], bar: { baz: "qux" } } }); let lastTxn = b1.txns.getLastSavedTxnProps(); chai.assert.isDefined(lastTxn); if (lastTxn) { chai.expect(lastTxn.props.source).to.be.equals("test"); chai.expect(lastTxn.props.description).to.be.equals("test description"); chai.expect(lastTxn.props.appData).to.not.be.undefined; chai.expect(lastTxn.props.appData?.test).to.be.eq("test"); chai.expect(lastTxn.props.appData?.foo).to.be.deep.eq([1, 2, 3]); chai.expect(lastTxn.props.appData?.bar).to.be.deep.eq({ baz: "qux" }); chai.expect(lastTxn.nextId).to.be.undefined; chai.expect(lastTxn.prevId).to.be.undefined; chai.expect(lastTxn.type).to.be.eq("Data"); chai.expect(lastTxn.id).to.be.eq('0x100000000'); chai.expect(lastTxn.reversed).to.be.false; chai.expect(lastTxn.grouped).to.be.false; } await testIModel.insertElement(b1Txn); b1Txn.saveChanges({ source: "test2", description: "test description 2", appData: { test: "test 2", foo: [11, 12, 13], bar: { baz: "qux2" } } }); lastTxn = b1.txns.getLastSavedTxnProps(); chai.assert.isDefined(lastTxn); if (lastTxn) { chai.expect(lastTxn.props.source).to.be.equals("test2"); chai.expect(lastTxn.props.description).to.be.equals("test description 2"); chai.expect(lastTxn.props.appData).to.not.be.undefined; chai.expect(lastTxn.props.appData?.test).to.be.eq("test 2"); chai.expect(lastTxn.props.appData?.foo).to.be.deep.eq([11, 12, 13]); chai.expect(lastTxn.props.appData?.bar).to.be.deep.eq({ baz: "qux2" }); chai.expect(lastTxn.nextId).to.be.undefined; chai.expect(lastTxn.prevId).to.be.equal('0x100000000'); chai.expect(lastTxn.type).to.be.eq("Data"); chai.expect(lastTxn.id).to.be.eq('0x100000001'); chai.expect(lastTxn.reversed).to.be.false; chai.expect(lastTxn.grouped).to.be.false; } await testIModel.insertElement(b1Txn); b1Txn.saveChanges("new element"); lastTxn = b1.txns.getLastSavedTxnProps(); chai.assert.isDefined(lastTxn); if (lastTxn) { chai.expect(lastTxn.props.source).is.undefined; chai.expect(lastTxn.props.description).to.be.equals("new element"); chai.expect(lastTxn.props.appData).to.be.undefined; chai.expect(lastTxn.nextId).to.be.undefined; chai.expect(lastTxn.prevId).to.be.equal('0x100000001'); chai.expect(lastTxn.type).to.be.eq("Data"); chai.expect(lastTxn.id).to.be.eq('0x100000002'); chai.expect(lastTxn.reversed).to.be.false; chai.expect(lastTxn.grouped).to.be.false; } await b1.pushChanges({ description: "new element" }); chai.expect(b1.txns.isUndoPossible).is.false; chai.expect(b1.txns.isRedoPossible).is.false; lastTxn = b1.txns.getLastSavedTxnProps(); chai.assert.isUndefined(lastTxn); }); it("direct / indirect", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "direct indirect"); const directElId = await testIModel.insertElement(b1Txn); const indirectElId = await testIModel.insertElement(b1Txn, true); chai.expect(directElId).to.not.be.undefined; chai.expect(indirectElId).to.not.be.undefined; b1Txn.saveChanges({ description: "insert element 1 direct and 1 indirect" }); const txn = b1.txns.getLastSavedTxnProps(); chai.assert.isDefined(txn); if (txn) { let checkCount = 0; const reader = SqliteChangesetReader.openTxn({ txnId: txn?.id, db: b1 }); while (reader.step()) { if (reader.primaryKeyValues.length === 0) continue; if (reader.tableName !== "bis_Element") continue; const iid = reader.primaryKeyValues[0]; if (iid === directElId) { chai.expect(reader.isIndirect).to.be.false; } if (iid === indirectElId) { chai.expect(reader.isIndirect).to.be.true; } checkCount++; } chai.expect(checkCount).to.be.equals(2); } await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" }); chai.expect(b1.txns.isUndoPossible).is.false; chai.expect(b1.txns.isRedoPossible).is.false; const lastTxn = b1.txns.getLastSavedTxnProps(); chai.assert.isUndefined(lastTxn); }); it("rebase handler", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "rebase handler b1"); const b2Txn = startTestTxn(b2, "rebase handler b2"); const e1 = await testIModel.insertElement(b1Txn); const e2 = await testIModel.insertElement(b1Txn, true); b1Txn.saveChanges(); await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" }); await b2.pullChanges(); await testIModel.updateElement(b1Txn, e1); await testIModel.updateElement(b1Txn, e2, true); b1Txn.saveChanges(); await b1.pushChanges({ description: "update element 1 direct and 1 indirect" }); await testIModel.insertElement(b2Txn); await testIModel.insertElement(b2Txn, true); b2Txn.saveChanges("first change"); await testIModel.insertElement(b2Txn); await testIModel.insertElement(b2Txn, true); b2Txn.saveChanges("second change"); await testIModel.insertElement(b2Txn); await testIModel.insertElement(b2Txn, true); b2Txn.saveChanges("third change"); b2.txns.rebaser.setCustomHandler({ shouldReinstate: (_txn) => { return true; }, recompute: async (_txn) => { await testIModel.insertElement(b2Txn); await testIModel.insertElement(b2Txn, true); }, }); await b1.pullChanges(); }); it("stash & drop", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "stash drop"); const e1 = await testIModel.insertElement(b1Txn); b1Txn.saveChanges(); await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" }); const e2 = await testIModel.insertElement(b1Txn); b1Txn.saveChanges(); await b1.pushChanges({ description: "insert element 1 direct and 1 indirect" }); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(`first`); await testIModel.updateElement(b1Txn, e1); b1Txn.saveChanges(`second`); await testIModel.deleteElement(b1Txn, e2); b1Txn.saveChanges(`third`); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(`fourth`); const stash1 = await StashManager.stash({ db: b1, description: "stash test 1" }); chai.expect(stash1).to.exist; chai.assert(Guid.isGuid(stash1.id)); chai.expect(stash1.description).to.equals("stash test 1"); chai.expect(stash1.briefcaseId).equals(b1.briefcaseId); chai.expect(stash1.iModelId).to.equals(b1.iModelId); chai.expect(stash1.timestamp).to.exist; chai.expect(stash1.description).to.exist; chai.expect(stash1.hash).length(64); chai.expect(stash1.parentChangeset).to.exist; chai.expect(stash1.idSequences.element).to.equals("0x30000000004"); chai.expect(stash1.idSequences.instance).to.equals("0x30000000000"); chai.expect(stash1.acquiredLocks).equals(4); chai.expect(stash1.txns).to.exist; chai.expect(stash1.txns).to.have.lengthOf(4); chai.expect(stash1.txns[0].props.description).to.equal("first"); chai.expect(stash1.txns[1].props.description).to.equal("second"); chai.expect(stash1.txns[2].props.description).to.equal("third"); chai.expect(stash1.txns[3].props.description).to.equal("fourth"); chai.expect(stash1.txns[0].id).to.equals("0x100000000"); chai.expect(stash1.txns[1].id).to.equals("0x100000001"); chai.expect(stash1.txns[2].id).to.equals("0x100000002"); chai.expect(stash1.txns[3].id).to.equals("0x100000003"); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(`fifth`); await testIModel.updateElement(b1Txn, e1); b1Txn.saveChanges(`sixth`); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(`seventh`); const stash2 = await StashManager.stash({ db: b1, description: "stash test 2" }); chai.expect(stash2).to.exist; chai.expect(stash2.description).to.equals("stash test 2"); chai.expect(stash2.hash).length(64); chai.expect(stash2.parentChangeset).to.exist; chai.expect(stash2.idSequences.element).to.equals("0x30000000006"); chai.expect(stash2.idSequences.instance).to.equals("0x30000000000"); chai.expect(stash2.acquiredLocks).equals(4); chai.expect(stash2.txns).to.exist; chai.expect(stash2.txns).to.have.lengthOf(7); chai.expect(stash2.txns[0].props.description).to.equal("first"); chai.expect(stash2.txns[1].props.description).to.equal("second"); chai.expect(stash2.txns[2].props.description).to.equal("third"); chai.expect(stash2.txns[3].props.description).to.equal("fourth"); chai.expect(stash2.txns[4].props.description).to.equal("fifth"); chai.expect(stash2.txns[5].props.description).to.equal("sixth"); chai.expect(stash2.txns[6].props.description).to.equal("seventh"); chai.expect(stash2.txns[0].id).to.equals("0x100000000"); chai.expect(stash2.txns[1].id).to.equals("0x100000001"); chai.expect(stash2.txns[2].id).to.equals("0x100000002"); chai.expect(stash2.txns[3].id).to.equals("0x100000003"); chai.expect(stash2.txns[4].id).to.equals("0x100000004"); chai.expect(stash2.txns[5].id).to.equals("0x100000005"); chai.expect(stash2.txns[6].id).to.equals("0x100000006"); const stashes = StashManager.getStashes(b1); chai.expect(stashes).to.have.lengthOf(2); chai.expect(stashes[0].description).to.equals("stash test 2"); chai.expect(stashes[1].description).to.equals("stash test 1"); chai.expect(stashes[0]).to.deep.equal(stash2); chai.expect(stashes[1]).to.deep.equal(stash1); StashManager.dropAllStashes(b1); chai.expect(StashManager.getStashes(b1)).to.have.lengthOf(0); }); it("recursively calling withIndirectTxnMode()", async () => { const b1 = await testIModel.openBriefcase(); chai.expect(b1.txns.getMode()).to.equal("direct"); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("direct"); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("indirect"); }); }); chai.expect(b1.txns.getMode()).to.equal("direct"); chai.expect(() => b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); throw new Error("Test error"); })).to.throw(); chai.expect(b1.txns.getMode()).to.equal("direct"); }); it("should fail to importSchemas() & importSchemaStrings() in indirect scope", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "indirect schema import failures"); const schema = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="MySchema" alias="ms1" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="my_class"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="prop1" typeName="string" /> </ECEntityClass> </ECSchema>`; const schemaFile = path.join(KnownTestLocations.outputDir, "MySchema.01.00.00.ecschema.xml"); if (existsSync(schemaFile)) { unlinkSync(schemaFile); } writeFileSync(schemaFile, schema, { encoding: "utf8" }); b1Txn.end("abandon"); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { await b1.importSchemas([schema]); })).to.be.rejectedWith("Cannot import schemas while in an indirect change scope"); b1Txn.start(); b1Txn.end("abandon"); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { await b1.importSchemaStrings([schema]); })).to.be.rejectedWith("Cannot import schemas while in an indirect change scope"); await b1.importSchemaStrings([schema]); await b1.pushChanges({ description: "import schema" }); }); it("should fail to saveChanges() & pushChanges() in indirect scope", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "indirect save push failures"); await testIModel.insertElement(b1Txn); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { b1Txn.saveChanges(); })).to.be.rejectedWith("Cannot save changes while in an indirect change scope"); chai.expect(() => b1.txns.withIndirectTxnMode(() => { b1Txn.saveChanges(); })).to.be.throws("Cannot save changes while in an indirect change scope"); b1Txn.saveChanges(); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { await b1.pushChanges({ description: "test" }); })).to.be.rejectedWith("Cannot pull and apply changeset while in an indirect change scope"); await b1.pushChanges({ description: "test" }); }); it("should fail to saveFileProperty/deleteFileProperty in indirect scope", async () => { // pull/push/saveFileProperty/deleteFileProperty should be called inside indirect change scope. const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "indirect file property failures"); b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "Hello, World"); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "This should fail 1"); })).to.be.rejectedWith("Cannot save file property while in an indirect change scope"); chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World"); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { b1Txn.deleteFileProperty({ namespace: "test", name: "test" }); })).to.be.rejectedWith("Cannot save file property while in an indirect change scope"); chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World"); chai.expect(() => b1.txns.withIndirectTxnMode(() => { b1Txn.saveFileProperty({ namespace: "test", name: "test" }, "This should fail 2"); })).to.be.throws("Cannot save file property while in an indirect change scope"); chai.expect(b1.queryFilePropertyString({ namespace: "test", name: "test" })).to.equal("Hello, World"); chai.expect(() => b1.txns.withIndirectTxnMode(() => { b1Txn.deleteFileProperty({ namespace: "test", name: "test" }); })).to.be.throws("Cannot save file property while in an indirect change scope"); b1Txn.saveChanges(); }); it("recursively calling withIndirectTxnModeAsync()", async () => { const b1 = await testIModel.openBriefcase(); chai.expect(b1.txns.getMode()).to.equal("direct"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("direct"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); await b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("indirect"); }); chai.expect(b1.txns.getMode()).to.equal("indirect"); }); }); b1.txns.withIndirectTxnMode(() => { chai.expect(b1.txns.getMode()).to.equal("indirect"); }); }); chai.expect(b1.txns.getMode()).to.equal("direct"); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { chai.expect(b1.txns.getMode()).to.equal("indirect"); throw new Error("Test error"); })).rejectedWith(Error); chai.expect(b1.txns.getMode()).to.equal("direct"); }); it("should restore mutually exclusive stashes", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "restore mutually exclusive stashes"); // stash 1 const e1 = await testIModel.insertElement(b1Txn); chai.expect(e1).to.exist; b1Txn.saveChanges("first"); const stash1 = await StashManager.stash({ db: b1, description: "stash test 1", discardLocalChanges: true, retainLocks: true }); chai.expect(stash1).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; // stash 2 const e2 = await testIModel.insertElement(b1Txn); chai.expect(e2).to.exist; b1Txn.saveChanges("second"); const stash2 = await StashManager.stash({ db: b1, description: "stash test 2", discardLocalChanges: true, retainLocks: true }); chai.expect(stash2).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; // stash 3 const e3 = await testIModel.insertElement(b1Txn); chai.expect(e3).to.exist; b1Txn.saveChanges("third"); const stash3 = await StashManager.stash({ db: b1, description: "stash test 3", discardLocalChanges: true, retainLocks: true }); chai.expect(stash3).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; chai.expect(e1).not.equals(e2); chai.expect(e1).not.equals(e3); chai.expect(e2).not.equals(e3); const stashes = StashManager.getStashes(b1); chai.expect(stashes).to.have.lengthOf(3); chai.expect(stashes[0].description).to.equals("stash test 3"); chai.expect(stashes[1].description).to.equals("stash test 2"); chai.expect(stashes[2].description).to.equals("stash test 1"); // restore stash 1 await StashManager.restore({ db: b1, stash: stash1 }); chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; // restore stash 2 await StashManager.restore({ db: b1, stash: stash2 }); chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.elements.tryGetElement(e2)).to.exist; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; // restore stash 3 await StashManager.restore({ db: b1, stash: stash3 }); chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.elements.tryGetElement(e3)).to.exist; }); it("should restore stash in any order", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "restore stash any order"); // stash 1 const e1 = await testIModel.insertElement(b1Txn); chai.expect(e1).to.exist; b1Txn.saveChanges("first"); // do not discard local changes const stash1 = await StashManager.stash({ db: b1, description: "stash test 1" }); chai.expect(stash1).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.txns.isUndoPossible).to.be.true; chai.expect(b1.txns.isRedoPossible).to.be.false; // stash 2 const e2 = await testIModel.insertElement(b1Txn); chai.expect(e2).to.exist; b1Txn.saveChanges("second"); // do not discard local changes const stash2 = await StashManager.stash({ db: b1, description: "stash test 2" }); chai.expect(stash2).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.exist; chai.expect(b1.txns.isUndoPossible).to.be.true; chai.expect(b1.txns.isRedoPossible).to.be.false; // stash 3 const e3 = await testIModel.insertElement(b1Txn); chai.expect(e3).to.exist; b1Txn.saveChanges("third"); // do not discard local changes const stash3 = await StashManager.stash({ db: b1, description: "stash test 3" }); chai.expect(stash3).to.exist; chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.exist; chai.expect(b1.elements.tryGetElement(e3)).to.exist; chai.expect(b1.txns.isUndoPossible).to.be.true; chai.expect(b1.txns.isRedoPossible).to.be.false; const stashes = StashManager.getStashes(b1); chai.expect(stashes).to.have.lengthOf(3); chai.expect(stashes[0].description).to.equals("stash test 3"); chai.expect(stashes[1].description).to.equals("stash test 2"); chai.expect(stashes[2].description).to.equals("stash test 1"); await b1.discardChanges({ retainLocks: true }); chai.expect(b1.elements.tryGetElement(e1)).to.undefined; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; chai.expect(b1.txns.isUndoPossible).to.be.false; chai.expect(b1.txns.isRedoPossible).to.be.false; // restore stash 1 await StashManager.restore({ db: b1, stash: stash1 }); chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.undefined; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; // restore stash 2 await StashManager.restore({ db: b1, stash: stash2 }); chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.exist; chai.expect(b1.elements.tryGetElement(e3)).to.undefined; // restore stash 3 await StashManager.restore({ db: b1, stash: stash3 }); chai.expect(b1.elements.tryGetElement(e1)).to.exist; chai.expect(b1.elements.tryGetElement(e2)).to.exist; chai.expect(b1.elements.tryGetElement(e3)).to.exist; }); it("should restore stash when briefcase has advanced to latest changeset", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "restore stash latest changeset b1"); const b2Txn = startTestTxn(b2, "restore stash latest changeset b2"); chai.expect(b1.changeset.index).to.equals(2); chai.expect(b2.changeset.index).to.equals(2); const e1 = await testIModel.insertElement(b1Txn); chai.expect(e1).to.exist; b1Txn.saveChanges(); await b1.pushChanges({ description: `${e1} inserted` }); chai.expect(b1.changeset.index).to.equals(3); const e2 = await testIModel.insertElement(b2Txn); chai.expect(e2).to.exist; b2Txn.saveChanges(); chai.expect(b2.elements.tryGetElement(e1)).to.undefined; chai.expect(b2.elements.tryGetElement(e2)).to.exist; const b2Stash1 = await StashManager.stash({ db: b2, description: "stash test 1", discardLocalChanges: true }); chai.expect(b2Stash1.parentChangeset.index).to.equals(2); chai.expect(b2.elements.tryGetElement(e1)).to.undefined; chai.expect(b2.elements.tryGetElement(e2)).to.undefined; await b2.pullChanges(); chai.expect(b2.changeset.index).to.equals(3); chai.expect(b2.elements.tryGetElement(e1)).to.exist; chai.expect(b2.elements.tryGetElement(e2)).to.undefined; // stash restore should downgrade briefcase to older changeset as specified in stash await StashManager.restore({ db: b2, stash: b2Stash1 }); chai.expect(b2.changeset.index).to.equals(2); chai.expect(b2.elements.tryGetElement(e1)).to.undefined; chai.expect(b2.elements.tryGetElement(e2)).to.exist; await b2.pullChanges(); chai.expect(b2.changeset.index).to.equals(3); chai.expect(b2.elements.tryGetElement(e1)).to.exist; chai.expect(b2.elements.tryGetElement(e2)).to.exist; await b2.pushChanges({ description: "test" }); chai.expect(b2.changeset.index).to.equals(4); }); it("restore stash that has element changed by another briefcase", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "restore stash changed element b1"); const b2Txn = startTestTxn(b2, "restore stash changed element b2"); chai.expect(b1.changeset.index).to.equals(2); chai.expect(b2.changeset.index).to.equals(2); const e1 = await testIModel.insertElement(b1Txn); chai.expect(e1).to.exist; b1Txn.saveChanges(); await b1.pushChanges({ description: `${e1} inserted` }); chai.expect(b1.changeset.index).to.equals(3); await b2.pullChanges(); chai.expect(b2.changeset.index).to.equals(3); await testIModel.updateElement(b2Txn, e1); b2Txn.saveChanges(); chai.expect(b2.locks.holdsExclusiveLock(e1)).to.be.true; const b2Stash1 = await StashManager.stash({ db: b2, description: "stash test 1", discardLocalChanges: true }); chai.expect(b2Stash1.parentChangeset.index).to.equals(3); chai.expect(b2.locks.holdsExclusiveLock(e1)).to.be.false; // stash release lock so b2 should have released lock and b1 should be able to update. await testIModel.updateElement(b1Txn, e1); b1Txn.saveChanges(); // restore stash should fail because of lock not obtained on e1 await chai.expect(StashManager.restore({ db: b2, stash: b2Stash1 })).to.be.rejectedWith("exclusive lock is already held"); // push b1 changes to release lock await b1.pushChanges({ description: `${e1} inserted` }); // restore stash should fail because pull is required to obtain lock await chai.expect(StashManager.restore({ db: b2, stash: b2Stash1 })).to.be.rejectedWith("pull is required to obtain lock"); await b2.pullChanges(); chai.expect(b2.changeset.index).to.equals(4); const elBefore = b2.elements.tryGetElementProps(e1); chai.expect(elBefore.prop1).to.equals("3"); // restore stash should succeed as now it can obtain lock await StashManager.restore({ db: b2, stash: b2Stash1 }); const elAfter = b2.elements.tryGetElementProps(e1); chai.expect(elAfter.prop1).to.equals("2"); await b2.pushChanges({ description: `${e1} updated` }); }); it("schema change should not be stashed", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "schema change should not be stashed"); const schema1 = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="ts" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="a1"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="prop1" typeName="string" /> <ECProperty propertyName="prop2" typeName="string" /> </ECEntityClass> <ECEntityClass typeName="A1Recipe2d"> <BaseClass>bis:TemplateRecipe2d</BaseClass> <ECProperty propertyName="prop1" typeName="string" /> </ECEntityClass> <ECRelationshipClass typeName="A1OwnsA1" modifier="None" strength="embedding"> <BaseClass>bis:ElementOwnsChildElements</BaseClass> <Source multiplicity="(0..1)" roleLabel="owns" polymorphic="true"> <Class class="a1"/> </Source> <Target multiplicity="(0..*)" roleLabel="is owned by" polymorphic="false"> <Class class="a1"/> </Target> </ECRelationshipClass> </ECSchema>`; await importSchemaStrings(b1Txn, [schema1]); await chai.expect(StashManager.stash({ db: b1, description: "stash test 1" })).to.not.rejectedWith("Bad Arg: Pending schema changeset stashing is not currently supported"); }); it("abort rebase", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "abort rebase b1"); const b2Txn = startTestTxn(b2, "abort rebase b2"); const e1 = await testIModel.insertElement(b1Txn); b1Txn.saveChanges(); await b1.pushChanges({ description: `${e1} inserted` }); const e2 = await testIModel.insertElement(b2Txn); chai.expect(e2).to.exist; let e3 = ""; b2Txn.saveChanges(); b2.txns.rebaser.setCustomHandler({ shouldReinstate: (_txnProps) => { return true; }, recompute: async (_txnProps) => { chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.true; e3 = await testIModel.insertElement(b2Txn); throw new Error("Rebase failed"); }, }); chai.expect(b2.elements.tryGetElementProps(e1)).to.undefined; chai.expect(b2.elements.tryGetElementProps(e2)).to.exist; chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined; chai.expect(b2.changeset.index).to.equals(2); await chai.expect(b2.pullChanges()).to.be.rejectedWith("Rebase failed"); chai.expect(b2.changeset.index).to.equals(3); chai.expect(e3).to.exist; chai.expect(b2.elements.tryGetElementProps(e1)).to.exist; // came from incoming changeset chai.expect(b2.elements.tryGetElementProps(e2)).to.undefined; // was local change and reversed during rebase. chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined; // was insert by reCompute() but due to exception the rebase attempt was abandoned. chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.true; chai.expect(b2.txns.rebaser.canAbort()).is.true; await b2.txns.rebaser.abort(); chai.expect(b2.changeset.index).to.equals(2); chai.expect(b2.elements.tryGetElementProps(e1)).to.undefined; // reset briefcase should move tip back to where it was before pull chai.expect(b2.elements.tryGetElementProps(e2)).to.exist; // abort should put back e2 which was only change at the time of pull chai.expect(b2.elements.tryGetElementProps(e3)).to.undefined; // add by rebase so should not exist either chai.expect(BriefcaseManager.containsRestorePoint(b2, BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)).is.false; }); it("calling discardChanges() from inside indirect scope is not allowed", async () => { const b1 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "discard changes inside indirect scope"); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(); const p1 = b1.txns.withIndirectTxnModeAsync(async () => { await b1.discardChanges(); }); await chai.expect(p1).to.be.rejectedWith("Cannot discard changes when there are indirect changes"); b1Txn.saveChanges(); }); it("calling discardChanges() during rebasing is not allowed", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "discard changes during rebasing b1"); const b2Txn = startTestTxn(b2, "discard changes during rebasing b2"); await testIModel.insertElement(b1Txn); await testIModel.insertElement(b1Txn); b1Txn.saveChanges(); await b1.pushChanges({ description: "inserted element" }); await testIModel.insertElement(b2Txn); await testIModel.insertElement(b2Txn); b2Txn.saveChanges(); b2.txns.rebaser.setCustomHandler({ shouldReinstate: (_txnProps) => { return true; }, recompute: async (_txnProps) => { chai.expect(b2.txns.rebaser.isAborting).is.false; await b2.discardChanges(); }, }); chai.expect(b2.txns.rebaser.isAborting).is.false; const p1 = b2.pullChanges(); await chai.expect(p1).to.be.rejectedWith("Cannot discard changes while a rebase is in progress"); chai.expect(b2.txns.rebaser.canAbort()).is.true; await b2.txns.rebaser.abort(); }); it("getStash() should throw exception", async () => { const b1 = await testIModel.openBriefcase(); chai.expect(() => StashManager.getStash({ db: b1, stash: "invalid_stash" })).to.throw("No stashes exist for this briefcase"); chai.expect(StashManager.tryGetStash({ db: b1, stash: "invalid_stash" })).to.be.undefined; }); it("edge case: a indirect update can cause FK violation", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); const b1Txn = startTestTxn(b1, "indirect update fk violation b1"); const b2Txn = startTestTxn(b2, "indirect update fk violation b2"); const parentId = await testIModel.insertElement(b1Txn); const childId = await testIModel.insertElementEx(b1Txn, { parent: { id: parentId, relClassName: "TestDomain:A1OwnsA1" } }); b1Txn.saveChanges("insert parent and child"); await b1.pushChanges({ description: `inserted parent ${parentId} and child ${childId}` }); await b2.pull