UNPKG

@itwin/core-backend

Version:
899 lines 220 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { Id64 } from "@itwin/core-bentley"; import { Code, IModel, QueryRowFormat, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import { HubWrappers, IModelTestUtils, KnownTestLocations } from ".."; import { BriefcaseManager, ChannelControl, DrawingCategory, IModelJsFs } from "../../core-backend"; import { EditTxn, withEditTxn } from "../../EditTxn"; import { HubMock } from "../../internal/HubMock"; import { EntityClass } from "@itwin/ecschema-metadata"; import { TestUtils } from "../TestUtils"; function startTestTxn(iModel, description = "semantic rebase") { const txn = new EditTxn(iModel, description); txn.start(); return txn; } function endTestTxn(txn) { if (txn.isActive) txn.end("abandon"); } async function importSchemaStrings(txn, schemas) { if (txn.isActive) txn.saveChanges(); await txn.iModel.importSchemaStrings(schemas); } async function pushChanges(txn, description) { const briefcase = txn.iModel; endTestTxn(txn); await briefcase.pushChanges({ description }); } async function pullChanges(txn) { const briefcase = txn.iModel; endTestTxn(txn); await briefcase.pullChanges(); } /** * Test infrastructure for rebase tests in this file. * Manages two briefcases (far and local) */ class TestIModel { iModelId = ""; drawingModelId = ""; drawingCategoryId = ""; far; local; constructor(iModelId, drawingModelId, drawingCategoryId, far, local) { this.iModelId = iModelId; this.drawingModelId = drawingModelId; this.drawingCategoryId = drawingCategoryId; this.far = far; this.local = local; } /** Reusable schema definitions for testing rebase with schema transformations */ static schemas = { /** Base schema v01.00.00 with classes A, C, D */ v01x00x00: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.01 - Adds PropC2 to class C (trivial additive change) */ v01x00x01AddPropC2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC2" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.02 - Adds PropD2 to class D (trivial additive change) */ v01x00x02AddPropD2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC2" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> <ECProperty propertyName="PropD2" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.02 - Moves PropC from C to A (requires data transformation) on top of v01.00.01 */ v01x00x02MovePropCToA: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC2" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.03 - Builds on top of v01.00.02 and in addition moves PropD to base, so we can have incoming and local transforming changes */ v01x00x03MovePropCAndD: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.03" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC2" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD2" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.01 (incompatible variant) - Adds PropC3 instead of PropC2 to class C (same version) */ v01x00x01AddPropC3Incompatible: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC3" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.02 (incompatible variant) - Adds PropC3 instead of PropC2 to class C (higher version) */ v01x00x02AddPropC3Incompatible: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC3" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.02 (incompatible variant) - Adds PropC2 (higher version, different type) */ v01x00x02AddPropC2Incompatible: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC2" typeName="int"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01.00.03 - Adds PropC2 as string (used to test incompatibility when reinstated on top of v01.00.02 with PropC2:int) */ v01x00x03AddPropC2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.03" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropC2" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, }; /** Additional schemas for extended edge-case tests */ static extendedSchemas = { /** v01x00x01 - Adds CUniqueAspect class (ElementUniqueAspect subclass) with AspectProp for aspect rebase tests */ v01x00x01WithAspect: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="CUniqueAspect" modifier="None"> <BaseClass>bis:ElementUniqueAspect</BaseClass> <ECProperty propertyName="AspectProp" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x02 - Extends v01WithAspect by adding AspectProp2 to CUniqueAspect (trivial aspect schema evolution) */ v01x00x02WithAspectProp2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="CUniqueAspect" modifier="None"> <BaseClass>bis:ElementUniqueAspect</BaseClass> <ECProperty propertyName="AspectProp" typeName="string"/> <ECProperty propertyName="AspectProp2" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x01 - Adds new entity class E extending A with PropE (tests new class addition) */ v01x00x01AddClassE: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="E"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropE" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x02 - Extends v01AddClassE by adding PropE2 to class E */ v01x00x02AddClassEPropE2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="E"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropE" typeName="string"/> <ECProperty propertyName="PropE2" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */ v01x00x01MultiTypeProps: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropCInt" typeName="int"/> <ECProperty propertyName="PropCDouble" typeName="double"/> <ECProperty propertyName="PropCBool" typeName="boolean"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x02 - Extends v01MultiTypeProps by adding PropD2 to class D */ v01x00x02MultiTypePropsExtended: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropCInt" typeName="int"/> <ECProperty propertyName="PropCDouble" typeName="double"/> <ECProperty propertyName="PropCBool" typeName="boolean"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> <ECProperty propertyName="PropD2" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */ v01x00x02MultiTypePropsMovePropDToA: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropCInt" typeName="int"/> <ECProperty propertyName="PropCDouble" typeName="double"/> <ECProperty propertyName="PropCBool" typeName="boolean"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> </ECEntityClass> </ECSchema>`, /** v01x00x02 - Moves PropD from class D to class A (transforming change for D, analogous to MovePropCToA) */ v01x00x02MovePropDToA: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> </ECEntityClass> </ECSchema>`, /** v01x00x01 - Adds a binary property (PropCBin) to class C */ v01x00x01WithBinaryProp: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropCBin" typeName="binary"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> </ECEntityClass> </ECSchema>`, /** v01x00x02 - Extends v01x00x01WithBinaryProp with PropD2 on class D (trivial additive change) */ v01x00x02WithBinaryPropAndPropD2: `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/> <ECEntityClass typeName="A"> <BaseClass>bis:GraphicalElement2d</BaseClass> <ECProperty propertyName="PropA" typeName="string"/> </ECEntityClass> <ECEntityClass typeName="C"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropC" typeName="string"/> <ECProperty propertyName="PropCBin" typeName="binary"/> </ECEntityClass> <ECEntityClass typeName="D"> <BaseClass>A</BaseClass> <ECProperty propertyName="PropD" typeName="string"/> <ECProperty propertyName="PropD2" typeName="string"/> </ECEntityClass> </ECSchema>`, }; /** * Create and initialize a new test iModel with far and local briefcases. * @param testName Unique name for this test (passed to HubMock.startup) * @returns Fully initialized TestIModel with both briefcases open */ static async initialize(testName) { HubMock.startup(testName, KnownTestLocations.outputDir); let far; let local; try { const iModelId = await HubMock.createNewIModel({ accessToken: "far-user", iTwinId: HubMock.iTwinId, iModelName: testName, description: `Rebase schema update with data transform tests: ${testName}`, }); // Open far briefcase and use it for initialization far = await HubWrappers.downloadAndOpenBriefcase({ iTwinId: HubMock.iTwinId, iModelId, accessToken: "far-user", }); far.channels.addAllowedChannel(ChannelControl.sharedChannelName); // Initialize with base schema await far.importSchemaStrings([TestIModel.schemas.v01x00x00]); await far.pushChanges({ description: "import base schema" }); // Create model and category const modelCode = IModelTestUtils.getUniqueModelCode(far, "DrawingModel"); await far.locks.acquireLocks({ shared: IModel.dictionaryId }); const [drawingModelId, drawingCategoryId] = withEditTxn(far, "create model and category", (txn) => { const [, newDrawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, modelCode); const newDrawingCategoryId = DrawingCategory.insert(txn, IModel.dictionaryId, "DrawingCategory", new SubCategoryAppearance()); return [newDrawingModelId, newDrawingCategoryId]; }); await far.pushChanges({ description: "create model and category" }); // Open local briefcase local = await HubWrappers.downloadAndOpenBriefcase({ iTwinId: HubMock.iTwinId, iModelId, accessToken: "local-user", }); local.channels.addAllowedChannel(ChannelControl.sharedChannelName); return new TestIModel(iModelId, drawingModelId, drawingCategoryId, far, local); } catch (error) { if (local?.isOpen) local.close(); if (far?.isOpen) far.close(); HubMock.shutdown(); throw error; } } insertElement(txn, className, properties) { const briefcase = txn.iModel; const elementProps = { classFullName: className, model: this.drawingModelId, category: this.drawingCategoryId, code: Code.createEmpty(), ...properties, }; const element = briefcase.elements.createElement(elementProps); return txn.insertElement(element.toJSON()); } updateElement(txn, elementId, updates) { const briefcase = txn.iModel; const element = briefcase.elements.getElementProps(elementId); Object.assign(element, updates); txn.updateElement(element); } getElementProps(briefcase, elementId) { return briefcase.elements.getElementProps(elementId); } getModelProps(briefcase, modelId) { return briefcase.models.tryGetModelProps(modelId); } checkIfFolderExists(briefcase, txnId, isSchemaFolder) { if (isSchemaFolder) return BriefcaseManager.semanticRebaseSchemaFolderExists(briefcase, txnId); return BriefcaseManager.semanticRebaseDataFolderExists(briefcase, txnId); } checkifRebaseFolderExists(briefcase) { const folderPath = BriefcaseManager.getBasePathForSemanticRebaseLocalFiles(briefcase); return IModelJsFs.existsSync(folderPath); } /** * Insert a UniqueAspect onto an element within the given EditTxn. * @param txn Active EditTxn * @param elementId Owning element's Id * @param aspectClassName Full class name, e.g. "TestDomain:CUniqueAspect" * @param properties Additional property key/value pairs * @returns The new aspect's ECInstanceId */ insertAspect(txn, elementId, aspectClassName, properties) { const aspectProps = { classFullName: aspectClassName, element: { id: elementId, relClassName: "BisCore.ElementOwnsUniqueAspect" }, ...properties, }; return txn.insertAspect(aspectProps); } /** * Update a UniqueAspect property within the given EditTxn. * Reads the existing aspect, merges updates, writes back. */ updateAspect(txn, elementId, aspectClassName, updates) { const briefcase = txn.iModel; const aspects = briefcase.elements.getAspects(elementId, aspectClassName); chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to update"); const aspect = aspects[0]; Object.assign(aspect, updates); txn.updateAspect(aspect.toJSON()); } /** * Delete a UniqueAspect within the given EditTxn. * Reads the existing aspect, then deletes it by instanceId. */ deleteAspect(txn, elementId, aspectClassName) { const briefcase = txn.iModel; const aspects = briefcase.elements.getAspects(elementId, aspectClassName); chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to delete"); txn.deleteAspect(aspects[0].id); } /** * Read a UniqueAspect from a briefcase and return it as a plain object. */ getAspect(briefcase, elementId, aspectClassName) { const aspects = briefcase.elements.getAspects(elementId, aspectClassName); return aspects.length > 0 ? aspects[0] : undefined; } /** * Open and return a third briefcase connected to this iModel (useful for three-briefcase tests). * The caller is responsible for closing the returned briefcase. */ async openExtraBriefcase(accessToken = "extra-user") { const extra = await HubWrappers.downloadAndOpenBriefcase({ iTwinId: HubMock.iTwinId, iModelId: this.iModelId, accessToken, }); extra.channels.addAllowedChannel(ChannelControl.sharedChannelName); return extra; } /** * Execute an ECSql SELECT and collect all rows into a Map keyed by ECInstanceId. * The SELECT must list ECInstanceId as the first column. Any additional columns are captured * by the caller-supplied names and stored in the returned row objects. * * Rows are returned using {@link QueryRowFormat.UseJsPropertyNames} so: * ECInstanceId → row.id * ec_className(ECClassId) → row.className (when aliased as `className`) * PropA → row.propA * PropC2 → row.propC2 * etc. * * @param briefcase The open iModel to query. * @param ecsql Complete ECSql SELECT statement whose first projected column is ECInstanceId. */ static async queryToMap(briefcase, ecsql) { const result = new Map(); // eslint-disable-next-line @typescript-eslint/no-deprecated const reader = briefcase.createQueryReader(ecsql, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }); for await (const row of reader) { const r = row.toRow(); result.set(r.id, r); } return result; } shutdown() { this.far.close(); this.local.close(); HubMock.shutdown(); } } /** * Test suite for rebase logic with schema changes that require data transformations. */ describe("Semantic Rebase", function () { this.timeout(90000); // operations can be slow let t; before(async () => { await TestUtils.shutdownBackend(); // Automatically TestUtils.startBackend() is called before every test suite starts we need to shut tht down and startup our new TestUtils with semantic rebase on await TestUtils.startBackend({ useSemanticRebase: true }); }); afterEach(() => { if (t) { t.shutdown(); t = undefined; } }); after(async () => { await TestUtils.shutdownBackend(); await TestUtils.startBackend(); // restart normal backend so subsequent test suites aren't left without IModelHost }); it("local data changes onto incoming trivial schema change", async () => { t = await TestIModel.initialize("TrivialSchemaIncoming"); let localTxn = startTestTxn(t.local, "local data changes onto incoming trivial schema change local"); let farTxn = startTestTxn(t.far, "local data changes onto incoming trivial schema change far"); // Local creates an element await t.local.locks.acquireLocks({ shared: t.drawingModelId }); const elementId = t.insertElement(localTxn, "TestDomain:C", { propA: "value_a", propC: "value_c", }); localTxn.saveChanges("create element"); await pushChanges(localTxn, "create test element"); localTxn = startTestTxn(t.local, "local data changes onto incoming trivial schema change local"); // Far imports updated schema with new property PropC2 await pullChanges(farTxn); farTxn = startTestTxn(t.far, "local data changes onto incoming trivial schema change far"); await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]); // Verify that we're holding a shared lock (not exclusive) for semantic rebase chai.expect(t.far.locks.holdsSharedLock(IModel.repositoryModelId)).to.be.true; chai.expect(t.far.holdsSchemaLock).to.be.false; const txnProps = t.far.txns.getLastSavedTxnProps(); chai.expect(txnProps).to.not.be.undefined; chai.expect(txnProps.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true; await pushChanges(farTxn, "add PropC2 to class C"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there // Local makes local changes to the element await t.local.locks.acquireLocks({ exclusive: elementId }); t.updateElement(localTxn, elementId, { propA: "local_update_a" }); localTxn.saveChanges("local update to propA"); // Local pulls and rebases local changes onto incoming schema change await pullChanges(localTxn); // Verify: local changes preserved, schema updated const element = t.getElementProps(t.local, elementId); chai.expect(element.propA).to.equal("local_update_a", "Local property update should be preserved"); chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved"); const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.01", "Schema should be updated to v01.00.01"); }); it("local trivial schema change onto incoming data changes", async () => { t = await TestIModel.initialize("TrivialSchemaLocal"); let localTxn = startTestTxn(t.local, "local trivial schema change onto incoming data changes local"); let farTxn = startTestTxn(t.far, "local trivial schema change onto incoming data changes far"); // Local creates an element await t.local.locks.acquireLocks({ shared: t.drawingModelId }); const elementId = t.insertElement(localTxn, "TestDomain:C", { propA: "value_a", propC: "value_c", }); localTxn.saveChanges("create element"); await pushChanges(localTxn, "create test element"); localTxn = startTestTxn(t.local, "local trivial schema change onto incoming data changes local"); // Local imports updated schema locally await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]); // Verify that we're holding a shared lock (not exclusive) for semantic rebase chai.expect(t.local.locks.holdsSharedLock(IModel.repositoryModelId)).to.be.true; chai.expect(t.local.holdsSchemaLock).to.be.false; const txnProps = t.local.txns.getLastSavedTxnProps(); chai.expect(txnProps).to.not.be.undefined; chai.expect(txnProps.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.local, txnProps.id, true)).to.be.true; // Far pulls element, then updates it await pullChanges(farTxn); farTxn = startTestTxn(t.far, "local trivial schema change onto incoming data changes far"); await t.far.locks.acquireLocks({ exclusive: elementId }); t.updateElement(farTxn, elementId, { propA: "far_update_a" }); farTxn.saveChanges("far update to propA"); await pushChanges(farTxn, "update element propA"); // Local pulls and rebases local schema change onto incoming data changes await pullChanges(localTxn); chai.expect(t.checkIfFolderExists(t.local, txnProps.id, true)).to.be.true; // after rebase the folder should be there until push is called // Verify: incoming data changes applied, local schema preserved const element = t.getElementProps(t.local, elementId); chai.expect(element.propA).to.equal("far_update_a", "Incoming property update should be applied"); chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved"); const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.01", "Local schema update should be preserved"); }); it("local data changes onto incoming data changes", async () => { t = await TestIModel.initialize("DataOntoData"); let localTxn = startTestTxn(t.local, "local data changes onto incoming data changes local"); let farTxn = startTestTxn(t.far, "local data changes onto incoming data changes far"); // Local creates two elements await t.local.locks.acquireLocks({ shared: t.drawingModelId }); const elementId1 = t.insertElement(localTxn, "TestDomain:C", { propA: "value_a1", propC: "value_c1", }); const elementId2 = t.insertElement(localTxn, "TestDomain:C", { propA: "value_a2", propC: "value_c2", }); localTxn.saveChanges("create elements"); chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used await pushChanges(localTxn, "create test elements"); localTxn = startTestTxn(t.local, "local data changes onto incoming data changes local"); chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used // Far updates first element await pullChanges(farTxn); farTxn = startTestTxn(t.far, "local data changes onto incoming data changes far"); await t.far.locks.acquireLocks({ exclusive: elementId1 }); t.updateElement(farTxn, elementId1, { propC: "far_update_c" }); farTxn.saveChanges("far update to propC"); chai.expect(t.checkifRebaseFolderExists(t.far)).to.be.false; // its data changes on both sides semantic rebase is not used await pushChanges(farTxn, "update element propC"); chai.expect(t.checkifRebaseFolderExists(t.far)).to.be.false; // its data changes on both sides semantic rebase is not used // Local makes local changes to second element await t.local.locks.acquireLocks({ exclusive: elementId2 }); t.updateElement(localTxn, elementId2, { propA: "local_update_a" }); localTxn.saveChanges("local update to propA"); chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used // Local pulls and rebases await pullChanges(localTxn); chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used // Verify: both changes applied to their respective elements const element1 = t.getElementProps(t.local, elementId1); chai.expect(element1.propA).to.equal("value_a1", "Element 1 propA should be unchanged"); chai.expect(element1.propC).to.equal("far_update_c", "Element 1 incoming update should be applied"); const element2 = t.getElementProps(t.local, elementId2); chai.expect(element2.propA).to.equal("local_update_a", "Element 2 local update should be preserved"); chai.expect(element2.propC).to.equal("value_c2", "Element 2 propC should be unchanged"); }); it("local trivial schema changes onto incoming trivial schema changes (local newer)", async () => { t = await TestIModel.initialize("TrivialSchemaLocalNewer"); const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming trivial schema changes local newer local"); const farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming trivial schema changes local newer far"); // Far imports v01.00.01 (adds PropC2) await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]); const txnProps = t.far.txns.getLastSavedTxnProps(); chai.expect(txnProps).to.not.be.undefined; chai.expect(txnProps.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true; await pushChanges(farTxn, "add PropC2 to class C"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there // Local imports v01.00.02 (adds PropC2 and PropD2 - newer) await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]); const txnPropsLocal = t.local.txns.getLastSavedTxnProps(); chai.expect(txnPropsLocal).to.not.be.undefined; chai.expect(txnPropsLocal.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true; // Local pulls and rebases await pullChanges(localTxn); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true; // after rebase the folder should be there because local is newer until push is called // Verify: local schema preserved (newer version) const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.02", "Local schema (newer) should be preserved"); }); it("local trivial schema changes onto incoming trivial schema changes (incoming newer)", async () => { t = await TestIModel.initialize("TrivialSchemaIncomingNewer"); const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming trivial schema changes incoming newer local"); const farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming trivial schema changes incoming newer far"); // Far imports v01.00.02 (adds PropC2 and PropD2 - newer) await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]); const txnProps = t.far.txns.getLastSavedTxnProps(); chai.expect(txnProps).to.not.be.undefined; chai.expect(txnProps.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true; await pushChanges(farTxn, "update schema to v01.00.02"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there // Local imports v01.00.01 (adds only PropC2 - older) await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]); const txnPropsLocal = t.local.txns.getLastSavedTxnProps(); chai.expect(txnPropsLocal).to.not.be.undefined; chai.expect(txnPropsLocal.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true; // Local pulls and rebases await pullChanges(localTxn); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.false; // after rebase the folder should not be there because incoming is newer so while rebasing it should be a no op chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because the rebase folder is deleted after rebase if it contains nothing // Verify: incoming schema preserved (newer version, local should not override) const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.02", "Incoming schema (newer) should win, local should not override"); }); it("local trivial schema changes onto incoming identical schema changes with data changes on both sides", async () => { t = await TestIModel.initialize("TrivialSchemaIdenticalWithData"); const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming identical schema changes with data local"); let farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming identical schema changes with data far"); // Far imports v01.00.01 (adds PropC2) and creates an element await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]); const txnProps = t.far.txns.getLastSavedTxnProps(); chai.expect(txnProps).to.not.be.undefined; chai.expect(txnProps.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true; await pushChanges(farTxn, "add PropC2 to class C"); farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming identical schema changes with data far"); chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there await t.far.locks.acquireLocks({ shared: t.drawingModelId }); const farElementId = t.insertElement(farTxn, "TestDomain:C", { propA: "far_value_a", propC: "far_value_c", propC2: "far_value_c2", }); farTxn.saveChanges("far creates element with new property"); await pushChanges(farTxn, "far creates element"); // Local imports the same v01.00.01 (adds PropC2) and creates an element await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]); const txnPropsLocal = t.local.txns.getLastSavedTxnProps(); chai.expect(txnPropsLocal).to.not.be.undefined; chai.expect(txnPropsLocal.type).to.equal("Schema"); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true; await t.local.locks.acquireLocks({ shared: t.drawingModelId }); const localElementId = t.insertElement(localTxn, "TestDomain:C", { propA: "local_value_a", propC: "local_value_c", propC2: "local_value_c2", }); localTxn.saveChanges("local creates element with new property"); // Local pulls and rebases await pullChanges(localTxn); chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.false; // after rebase the folder should not be there as both are identical chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because the rebase folder is deleted after rebase if it contains nothing // Verify: schema preserved (both sides identical) const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01"); // Verify: both elements exist with their original properties const farElement = t.getElementProps(t.local, farElementId); chai.expect(farElement.propA).to.equal("far_value_a", "Far element propA should be preserved"); chai.expect(farElement.propC).to.equal("far_value_c", "Far element propC should be preserved"); chai.expect(farElement.propC2).to.equal("far_value_c2", "Far element propC2 should be preserved"); const localElement = t.getElementProps(t.local, localElementId); chai.expect(localElement.propA).to.equal("local_value_a", "Local element propA should be preserved"); chai.expect(localElement.propC).to.equal("local_value_c", "Local element propC should be preserved"); chai.expect(localElement.propC2).to.equal("local_value_c2", "Local element propC2 should be preserved"); }); it("both add different properties, increment to same version number", async () => { t = await TestIModel.initialize("TrivialSchemaIncompatible"); const localTxn = startTestTxn(t.local, "both add different properties increment to same version local"); const farTxn = startTestTxn(t.far, "both add different properties increment to same version far"); await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]); await pushChanges(farTxn, "add PropC2 to class C"); await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC3Incompatible]); await pullChanges(localTxn); // TODO: this currently passes, because same version number means no upgrade is attempted //TODO: this should probably fail instead as both sides made incompatible changes to the same version, but this is unrelated to semantic rebase itself const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01"); }); it("both add compatible properties, local version number higher", async () => { t = await TestIModel.initialize("CompatibleSchemaLocalHigher"); const localTxn = startTestTxn(t.local, "both add compatible properties local version higher local"); const farTxn = startTestTxn(t.far, "both add compatible properties local version higher far"); // Far imports v01.00.01 (adds PropC2) await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]); await pushChanges(farTxn, "add PropC2 to class C"); // Local imports v01.00.02 (adds PropC2 and PropD2 - compatible higher version) await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]); // Local pulls and rebases await pullChanges(localTxn); // Verify: Local schema wins (higher version) const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.02", "Schema should be v01.00.02 (higher version wins)"); const classC = await t.local.schemaContext.getSchemaItem("TestDomain", "C", EntityClass); chai.expect(classC).to.not.be.undefined; chai.expect(await classC.getProperty("PropC2")).to.exist; const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass); chai.expect(classD).to.not.be.undefined; chai.expect(await classD.getProperty("PropD2")).to.exist; }); it("both add compatible properties, incoming version number higher", async () => { t = await TestIModel.initialize("CompatibleSchemaIncomingHigher"); const localTxn = startTestTxn(t.local, "both add compatible properties incoming version higher local"); const farTxn = startTestTxn(t.far, "both add compatible properties incoming version higher far"); // Far imports v01.00.02 (adds PropC2 and PropD2 - higher version) await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]); await pushChanges(farTxn, "update schema to v01.00.02"); // Local imports v01.00.01 (adds only PropC2 - compatible lower version) await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]); // Local pulls and rebases await pullChanges(localTxn); // Verify: Incoming schema wins (higher version) const schema = t.local.getSchemaProps("TestDomain"); chai.expect(schema.version).to.equal("01.00.02", "Schema should be v01.00.02 (higher version wins)"); const classC = await t.local.schemaContext.getSchemaItem("TestDomain", "C", EntityClass); chai.expect(classC).to.not.be.undefined; chai.expect(await classC.getProperty("PropC2")).to.exist; const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass); chai.expect(classD).to.not.be.undefined; chai.expect(await classD.getProperty("PropD2")).to.exist; }); it("both add same but incompatible property, local version number higher", async () => { t = await TestIModel.initialize("Tri