@itwin/core-backend
Version:
iTwin.js backend components
383 lines • 20.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/naming-convention */
import { assert } from "chai";
import * as fs from "fs";
import * as path from "path";
import { Guid, Logger, OpenMode } from "@itwin/core-bentley";
import { CodeScopeSpec, CodeSpec, ColorByName, DomainOptions, GeometryStreamBuilder, IModel, SubCategoryAppearance, } from "@itwin/core-common";
import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry";
import { _nativeDb, ChannelControl, IModelJsFs, PhysicalModel, SpatialCategory, StandaloneDb } from "../../core-backend";
import { IModelTestUtils, TestElementDrivesElement, TestPhysicalObject } from "../IModelTestUtils";
import { IModelNative } from "../../internal/NativePlatform";
export function copyFile(newName, pathToCopy) {
const newPath = path.join(path.dirname(pathToCopy), newName);
try {
fs.unlinkSync(newPath);
}
catch {
}
fs.copyFileSync(pathToCopy, newPath);
return newPath;
}
function assertRels(list, rels) {
assert.equal(list.length, rels.length);
for (let i = 0; i < rels.length; ++i) {
assert.equal(list[i].id, rels[i].id);
}
}
class DependencyCallbackResults {
beforeOutputs = [];
allInputsHandled = [];
rootChanged = [];
deletedDependency = [];
}
class TestHelper {
db;
dres = new DependencyCallbackResults();
removals = [];
codeSpecId = "";
physicalModelId = "";
spatialCategoryid = "";
constructor(testName, dbInfo) {
this.codeSpecId = dbInfo.codeSpecId;
this.physicalModelId = dbInfo.physicalModelId;
this.spatialCategoryid = dbInfo.spatialCategoryId;
const writeDbFileName = copyFile(`${testName}.bim`, dbInfo.seedFileName);
this.db = StandaloneDb.openFile(writeDbFileName, OpenMode.ReadWrite);
assert.isTrue(this.db !== undefined);
this.db.channels.addAllowedChannel(ChannelControl.sharedChannelName);
this.db[_nativeDb].enableTxnTesting();
assert.equal(this.db[_nativeDb].addChildPropagatesChangesToParentRelationship("TestBim", "ChildPropagatesChangesToParent"), 0);
this.setElementDependencyGraphCallbacks();
}
terminate() {
this.db.close();
assert.isFalse(this.db.isOpen);
this.removeElementDependencyGraphCallbacks();
}
makeElement(codeValue, parent) {
const builder = new GeometryStreamBuilder();
builder.appendGeometry(LineSegment3d.create(Point3d.createZero(), Point3d.create(5, 0, 0)));
return {
classFullName: "TestBim:TestPhysicalObject",
model: this.physicalModelId,
category: this.spatialCategoryid,
code: { spec: this.codeSpecId, scope: this.physicalModelId, value: codeValue },
intProperty: 100,
placement: {
origin: new Point3d(0, 0, 0),
angles: new YawPitchRollAngles(),
},
geom: builder.geometryStream,
parent,
};
}
insertElement(codeValue, parent) {
return this.db.elements.insertElement(this.makeElement(codeValue, parent));
}
updateElement(elid, newLabel) {
const ed2 = this.db.elements.getElement({ id: elid });
ed2.userLabel = newLabel;
this.db.elements.updateElement(ed2.toJSON());
}
fmtElem(elId) { return this.db.elements.getElement(elId).code.value; }
fmtRel(props) { return `${props.classFullName} ${this.fmtElem(props.sourceId)} --> ${this.fmtElem(props.targetId)}`; }
resetDependencyResults() {
this.dres = new DependencyCallbackResults();
}
setElementDependencyGraphCallbacks() {
this.removals.push(TestElementDrivesElement.deletedDependency.addListener((evProps) => {
Logger.logTrace("EDGTest", `_onDeletedDependency ${this.fmtRel(evProps)}`);
this.dres.deletedDependency.push(evProps);
}));
this.removals.push(TestElementDrivesElement.rootChanged.addListener((evProps, _im) => {
Logger.logTrace("EDGTest", `_onRootChanged ${this.fmtRel(evProps)}`);
this.dres.rootChanged.push(evProps);
}));
this.removals.push(TestPhysicalObject.beforeOutputsHandled.addListener((elId) => {
Logger.logTrace("EDGTest", `_onBeforeOutputsHandled ${this.fmtElem(elId)}`);
this.dres.beforeOutputs.push(elId);
}));
this.removals.push(TestPhysicalObject.allInputsHandled.addListener((elId) => {
Logger.logTrace("EDGTest", `_onAllInputsHandled ${this.fmtElem(elId)}`);
this.dres.allInputsHandled.push(elId);
}));
}
removeElementDependencyGraphCallbacks() {
this.removals.forEach((drop) => drop());
}
}
describe("ElementDependencyGraph", () => {
let testFileName;
let dbInfo;
const performUpgrade = (pathname) => {
const nativeDb = new IModelNative.platform.DgnDb();
const upgradeOptions = {
domain: DomainOptions.Upgrade,
schemaLockHeld: true,
};
nativeDb.openIModel(pathname, OpenMode.ReadWrite, upgradeOptions);
nativeDb.deleteAllTxns();
nativeDb.closeFile();
};
before(async () => {
IModelTestUtils.registerTestBimSchema();
// make a unique name for the output file so this test can be run in parallel
testFileName = IModelTestUtils.prepareOutputFile("ElementDependencyGraph", `${Guid.createValue()}.bim`);
const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
const schemaFileName = IModelTestUtils.resolveAssetFile("TestBim.ecschema.xml");
IModelJsFs.copySync(seedFileName, testFileName);
performUpgrade(testFileName);
const imodel = StandaloneDb.openFile(testFileName, OpenMode.ReadWrite);
await imodel.importSchemas([schemaFileName]); // will throw an exception if import fails
imodel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
const physicalModelId = PhysicalModel.insert(imodel, IModel.rootSubjectId, "EDGTestModel");
const codeSpecId = imodel.codeSpecs.insert(CodeSpec.create(imodel, "EDGTestCodeSpec", CodeScopeSpec.Type.Model));
const spatialCategoryId = SpatialCategory.insert(imodel, IModel.dictionaryId, "EDGTestSpatialCategory", new SubCategoryAppearance({ color: ColorByName.darkRed }));
dbInfo = { physicalModelId, codeSpecId, spatialCategoryId, seedFileName: testFileName };
imodel.saveChanges("");
imodel[_nativeDb].deleteAllTxns();
imodel.close();
});
after(() => {
IModelJsFs.removeSync(testFileName);
});
it("should invokeCallbacks EDE only", () => {
const helper = new TestHelper("EDE", dbInfo);
const e1id = helper.insertElement("e1");
const e2id = helper.insertElement("e2");
const e3id = helper.insertElement("e3");
helper.db.saveChanges(); // get the elements into the iModel
const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
for (const ede of [ede_1_2, ede_2_3]) {
ede.insert();
}
// The full graph:
// e1 --> e2 --> e3
helper.resetDependencyResults();
helper.db.saveChanges(); // this will react to EDE inserts only.
assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON()]); // we send out this callback even if only the relationship itself is new or changed.
helper.updateElement(e1id, "change e1");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id]);
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON()]);
helper.terminate();
});
it("should invokeCallbacks through parents only", () => {
const helper = new TestHelper("Parents", dbInfo);
const p2id = helper.insertElement("p2");
const e1id = helper.insertElement("e1", { id: p2id, relClassName: "TestBim.ChildPropagatesChangesToParent" });
helper.db.saveChanges(); // get the elements into the iModel
// The full graph:
// .-parent-> p2
// /
// e1
//
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
assertRels(helper.dres.rootChanged, []); // we send out this callback even if only the relationship itself is new or changed.
helper.updateElement(e1id, "change e1");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [p2id]);
assertRels(helper.dres.rootChanged, []);
helper.updateElement(p2id, "change p2");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, []); // only called on directly changed root elements
assert.deepEqual(helper.dres.allInputsHandled, []);
assertRels(helper.dres.rootChanged, []);
helper.terminate();
});
it("should invokeCallbacks through EDEs and parents", () => {
const helper = new TestHelper("EDEsAndParents", dbInfo);
const p2id = helper.insertElement("p2");
const p3id = helper.insertElement("p3");
const e1id = helper.insertElement("e1", { id: p2id, relClassName: "TestBim.ChildPropagatesChangesToParent" });
const e2id = helper.insertElement("e2");
const e3id = helper.insertElement("e3");
helper.db.saveChanges(); // get the elements into the iModel
const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
const ede_p2_p3 = TestElementDrivesElement.create(helper.db, p2id, p3id);
for (const ede of [ede_1_2, ede_2_3, ede_p2_p3]) {
ede.insert();
}
// db[_nativeDb].writeFullElementDependencyGraphToFile(`${writeDbFileName}.dot`);
// The full graph:
// .-parent-> p2 -EDE-> p3
// /
// e1 -EDE-> e2 -EDE-> e3
//
helper.resetDependencyResults();
helper.db.saveChanges(); // this will react to EDE inserts only.
assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_p2_p3.toJSON()]); // we send out this callback even if only the relationship itself is new or changed.
helper.updateElement(e1id, "change e1");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [e2id, p2id, e3id, p3id]);
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_p2_p3.toJSON()]);
helper.updateElement(p2id, "change p2");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [p2id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [p3id]);
assertRels(helper.dres.rootChanged, [ede_p2_p3.toJSON()]);
helper.updateElement(e2id, "change e2");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e2id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [e3id]);
assertRels(helper.dres.rootChanged, [ede_2_3.toJSON()]);
helper.terminate();
});
it("should invokeCallbacks through parents - geomodeler schema", () => {
const helper = new TestHelper("GeoModeler", dbInfo);
// The full graph:
// BoreholeSource -EDE-> GroundGeneration
// / parent
// Borehole
// / parent
// Material -EDE-> MaterialDepthRange
const boreholeSource = helper.insertElement("BoreholeSource");
const borehole = helper.insertElement("Borehole", { id: boreholeSource, relClassName: "TestBim.ChildPropagatesChangesToParent" });
const materialDepthRange = helper.insertElement("MaterialDepthRange", { id: borehole, relClassName: "TestBim.ChildPropagatesChangesToParent" });
const material = helper.insertElement("Material");
const groundGeneration = helper.insertElement("GroundGeneration");
helper.db.saveChanges(); // get the elements into the iModel
const ede_material_materialDepthRange = TestElementDrivesElement.create(helper.db, material, materialDepthRange);
const ede_boreholeSource_groundGeneration = TestElementDrivesElement.create(helper.db, boreholeSource, groundGeneration);
for (const ede of [ede_material_materialDepthRange, ede_boreholeSource_groundGeneration]) {
ede.insert();
}
helper.resetDependencyResults();
helper.db.saveChanges();
helper.updateElement(material, "change material");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [material]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [materialDepthRange, borehole, boreholeSource, groundGeneration]);
assertRels(helper.dres.rootChanged, [ede_material_materialDepthRange.toJSON(), ede_boreholeSource_groundGeneration.toJSON()]);
helper.terminate();
});
it("should invokeCallbacks many:1", () => {
const helper = new TestHelper("ManyToOne", dbInfo);
const e1id = helper.insertElement("e1");
const e11id = helper.insertElement("e11");
const e2id = helper.insertElement("e2");
const e21id = helper.insertElement("e21");
const e3id = helper.insertElement("e3");
const e4id = helper.insertElement("e4");
const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
const ede_11_2 = TestElementDrivesElement.create(helper.db, e11id, e2id);
const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
const ede_21_3 = TestElementDrivesElement.create(helper.db, e21id, e3id);
const ede_3_4 = TestElementDrivesElement.create(helper.db, e3id, e4id);
for (const ede of [ede_1_2, ede_11_2, ede_2_3, ede_21_3, ede_3_4]) {
ede.insert();
}
// The full graph:
// e21
// \
// e1 --> e2 --> e3 --> e4
// /
// e11
// On the very first validation, everything is new and is considered directly changed
// resulting graph:
// e21
// \
// e1 --> e2 --> e3 --> e4
// /
// e11
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e1id, e11id, e21id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_11_2.toJSON(), ede_2_3.toJSON(), ede_21_3.toJSON(), ede_3_4.toJSON()]);
// modify e4 directly. That is a leaf. None of its inputs are changed.
// resulting subgraph:
// *
// e4
//
//
helper.updateElement(e4id, "change e4");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, []); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, []);
assertRels(helper.dres.rootChanged, []);
// modify e3 directly.
// resulting subgraph:
//
//
// *
// e3 --> e4
//
//
helper.updateElement(e3id, "change e3");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e3id]); // only called on directly changed root elements.
assert.deepEqual(helper.dres.allInputsHandled, [e4id]);
assertRels(helper.dres.rootChanged, [ede_3_4.toJSON()]);
// modify e2 directly. That is a node in middle of the graph. None of its inputs is modified.
// resulting subgraph:
//
// *
// e2 --> e3 --> e4
//
//
//
helper.updateElement(e2id, "change e2");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e2id]); // only called on directly changed root elements
assert.deepEqual(helper.dres.allInputsHandled, [e3id, e4id]);
assertRels(helper.dres.rootChanged, [ede_2_3.toJSON(), ede_3_4.toJSON()]);
// Modify e1 directly. That should propagate to the rest of the nodes. Each should get an _onAllInputsHandled callback
// resulting graph:
//
// *
// e1 --> e2 --> e3 --> e4
//
helper.updateElement(e1id, "change e1");
helper.resetDependencyResults();
helper.db.saveChanges();
assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements
assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_3_4.toJSON()]);
// Modify e11 directly. That should propagate to the rest of the nodes. Each should get an _onAllInputsHandled callback
// resulting graph:
//
// > e2 --> e3 --> e4
// /
// e11
// *
//
// Note that the e1 -> e2 and e21 -> e3 edges are NOT in the sub-graph. These edges should be validated, nevertheless -- TBD
helper.updateElement(e11id, "change e11");
helper.resetDependencyResults();
helper.db.saveChanges();
// assert.deepEqual(helper.dres.directChange, []); // only called on directly changed non-root elements that have no directly changed inputs
assert.deepEqual(helper.dres.beforeOutputs, [e11id]); // only called on directly changed root elements
assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
assertRels(helper.dres.rootChanged, [ede_11_2.toJSON(), ede_2_3.toJSON(), ede_3_4.toJSON()]);
// assertRels(helper.dres.validateOutput, [ede_1_2, ede_21_3]); // this callback is made only on rels that not in the graph but share an output with another rel or have an output that was directly changed
helper.terminate();
});
});
//# sourceMappingURL=ElementDependencyGraph.test.js.map