From 787cf5aa8f00ff767606d35d94e1dad7c44d58cb Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 23 Sep 2025 13:15:35 -0400 Subject: [PATCH 01/17] WIP --- .../backend/src/test/hubaccess/Rebase.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index c4edf4ea0db..7213d107d5f 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -724,4 +724,64 @@ describe("rebase changes & stashing api", function (this: Suite) { await chai.expect(b2.pushChanges({ description: `deleted child ${childId} and inserted grandchild ${grandChildId}` })) .to.be.rejectedWith("Foreign key conflicts in ChangeSet. Aborting rebase."); }); + it.only("EDE", async () => { + const schema1 = ` + + + + bis:GraphicalElement2d + + + + + + bis:ElementOwnsChildElements + + + + + + + + `; + + const b1 = await testIModel.openBriefcase(); + await b1.importSchemaStrings([schema1]); + const model = testIModel.drawingModelId; + const category = testIModel.drawingCategoryId; + const code = Code.createEmpty(); + const n1 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, + op: "", var: "n1", val: 100 + } as NodeElementProps); + chai.expect(n1).to.exist; + const n2 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, + op: "", var: "n2", val: 200 + } as NodeElementProps); + chai.expect(n2).to.exist; + const n3 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, + op: "sum", var: "n3", val: 0 + } as NodeElementProps); + chai.expect(n3).to.exist; + + b1.relationships.insertInstance({ + classFullName: "EDE:InputDrivesOutput", + sourceId: n1, + targetId: n3, + }); + + b1.relationships.insertInstance({ + classFullName: "EDE:InputDrivesOutput", + sourceId: n2, + targetId: n3, + }); + + b1.txns.on + }); }); + + +interface NodeElementProps extends GeometricElement2dProps { + op: string; + var: string; + val: number; +} From 25a73dfdde93da99cba9ac71b414146f24644bf6 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 25 Sep 2025 14:20:56 -0400 Subject: [PATCH 02/17] fix --- core/backend/src/test/EDE.test.ts | 233 ++++++++++++++++++ .../backend/src/test/hubaccess/Rebase.test.ts | 65 +---- 2 files changed, 236 insertions(+), 62 deletions(-) create mode 100644 core/backend/src/test/EDE.test.ts diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts new file mode 100644 index 00000000000..85e58beaff2 --- /dev/null +++ b/core/backend/src/test/EDE.test.ts @@ -0,0 +1,233 @@ +import { DbResult, Id64String, IModelStatus } from "@itwin/core-bentley"; +import { Code, ElementProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; +import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry"; +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import { SpatialCategory } from "../Category"; +import { ChannelControl } from "../ChannelControl"; +import { ClassRegistry } from "../ClassRegistry"; +import { GeometricElement3d, PhysicalPartition } from "../Element"; +import { BriefcaseDb, IModelDb } from "../IModelDb"; +import { HubMock } from "../internal/HubMock"; +import { PhysicalModel } from "../Model"; +import { SubjectOwnsPartitionElements } from "../NavigationRelationship"; +import { ElementDrivesElement, ElementDrivesElementProps } from "../Relationship"; +import { Schema, Schemas } from "../Schema"; +import { HubWrappers } from "./IModelTestUtils"; +import { KnownTestLocations } from "./KnownTestLocations"; +chai.use(chaiAsPromised); + +export interface InputDrivesOutputProps extends ElementDrivesElementProps { + prop: string +} + +export interface NodeElementProps extends GeometricElement3dProps { + op: string; + val: number; +} + +export class InputDrivesOutput extends ElementDrivesElement { + public static override get className(): string { return "InputDrivesOutput"; } + protected constructor(props: InputDrivesOutputProps, iModel: IModelDb) { + super(props, iModel); + } + public static override onRootChanged(_props: RelationshipProps, _iModel: IModelDb): void { } + public static override onDeletedDependency(_props: RelationshipProps, _iModel: IModelDb): void { } +} + +export class NodeElement extends GeometricElement3d { + public op: string; + public val: number; + public static override get className(): string { return "Node"; } + protected constructor(props: NodeElementProps, iModel: IModelDb) { + super(props, iModel); + this.op = props.op; + this.val = props.val; + } + public override toJSON(): GeometricElement3dProps { + const val = super.toJSON() as NodeElementProps; + val.op = this.op; + val.val = this.val; + return val; + } + protected static override onAllInputsHandled(_id: Id64String, _iModel: IModelDb): void { } + protected static override onBeforeOutputsHandled(_id: Id64String, _iModel: IModelDb): void { } + public static generateGeometry(radius: number): GeometryStreamProps { + const builder = new GeometryStreamBuilder(); + const p1 = Point3d.createZero(); + const p2 = Point3d.createFrom({ x: radius, y: 0.0, z: 0.0 }); + const circle = LineSegment3d.create(p1, p2); + builder.appendGeometry(circle); + return builder.geometryStream; + } + public static getCategory(iModelDb: IModelDb) { + const categoryId = SpatialCategory.queryCategoryIdByName(iModelDb, IModelDb.dictionaryId, this.classFullName); + if (categoryId === undefined) + throw new IModelError(IModelStatus.NotFound, "Category not found"); + return iModelDb.elements.getElement(categoryId); + } +} + +export class NetworkSchema extends Schema { + public static override get schemaName(): string { return "Network"; } + public static registerSchema() { + if (this !== Schemas.getRegisteredSchema(NetworkSchema.schemaName)) { + Schemas.registerSchema(this); + ClassRegistry.register(NodeElement, this); + ClassRegistry.register(InputDrivesOutput, this); + } + } + + public static async importSchema(iModel: IModelDb): Promise { + if (iModel.querySchemaVersion("Network")) + return; + + const schema1 = ` + + + + bis:GraphicalElement3d + + + + + bis:ElementDrivesElement + + + + + + + + + `; + await iModel.importSchemaStrings([schema1]); + } +} + +export class Engine { + public static countNodes(iModelDb: IModelDb): number { + return iModelDb.withSqliteStatement("SELECT COUNT(*) FROM Network.Node", (stmt) => { + if (stmt.step() === DbResult.BE_SQLITE_ROW) { + return stmt.getValue(0).getInteger(); + } + return 0; + }); + } + private static async createPartition(iModelDb: IModelDb): Promise { + const parentId = new SubjectOwnsPartitionElements(IModel.rootSubjectId); + const modelId = IModel.repositoryModelId; + const modeledElementProps: ElementProps = { + classFullName: PhysicalPartition.classFullName, + parent: parentId, + model: modelId, + code: Code.createEmpty(), + userLabel: "NetworkPhysicalPartition" + }; + const modeledElement = iModelDb.elements.createElement(modeledElementProps); + await iModelDb.locks.acquireLocks({ shared: modelId }); + return iModelDb.elements.insertElement(modeledElement.toJSON()); + } + private static async createModel(iModelDb: IModelDb): Promise { + const partitionId = await this.createPartition(iModelDb); + const modeledElementRef = new RelatedElement({ id: partitionId }); + const newModel = iModelDb.models.createModel({ modeledElement: modeledElementRef, classFullName: PhysicalModel.classFullName }); + const newModelId = newModel.insert(); + return newModelId; + } + private static async createNodeCategory(iModelDb: IModelDb) { + const category = SpatialCategory.create(iModelDb, IModelDb.dictionaryId, NodeElement.classFullName); + return category.insert(); + } + public static async initialize(iModelDb: IModelDb) { + await NetworkSchema.importSchema(iModelDb); + NetworkSchema.registerSchema(); + const modelId = await this.createModel(iModelDb); + const categoryId = await this.createNodeCategory(iModelDb); + return { + modelId, + categoryId, + }; + } + + public static async insertNode(iModelDb: IModelDb, modelId: Id64String, name: string, op: string, val: number, location: Point3d, radius: number = 0.1) { + const props: NodeElementProps = { + classFullName: NodeElement.classFullName, + model: modelId, + code: Code.createEmpty(), + userLabel: name, + category: NodeElement.getCategory(iModelDb).id, + placement: { origin: location, angles: new YawPitchRollAngles() }, + geom: NodeElement.generateGeometry(radius), + op, + val, + }; + await iModelDb.locks.acquireLocks({ shared: modelId }); + return iModelDb.elements.insertElement(props); + } + public static async insertEdge(iModelDb: IModelDb, sourceId: Id64String, targetId: Id64String, prop: string) { + const props: InputDrivesOutputProps = { + classFullName: InputDrivesOutput.classFullName, + sourceId, + targetId, + prop, + status: 0, + priority: 0 + }; + return iModelDb.relationships.insertInstance(props); + } +} + +describe.only("EDE Tests", () => { + const briefcases: BriefcaseDb[] = []; + let iModelId: string; + async function openBriefcase(): Promise { + const iModelDb = await HubWrappers.downloadAndOpenBriefcase({ iTwinId: HubMock.iTwinId, iModelId }); + iModelDb.channels.addAllowedChannel(ChannelControl.sharedChannelName); + iModelDb.saveChanges(); + briefcases.push(iModelDb); + return iModelDb; + } + beforeEach(async () => { + HubMock.startup("TestIModel", KnownTestLocations.outputDir); + iModelId = await HubMock.createNewIModel({ iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" }); + }); + afterEach(async () => { + for (const briefcase of briefcases) { + briefcase.close(); + } + HubMock.shutdown(); + }); + + it("should insert and count nodes", async () => { + const b1 = await openBriefcase(); + const { modelId, categoryId } = await Engine.initialize(b1); + + chai.expect(modelId).to.equals("0x20000000001"); + chai.expect(categoryId).to.equals("0x20000000002"); + + b1.saveChanges(); + await b1.pullChanges(); + + // Node1 ---[1-2]---> Node2 ---[2-3]---> Node3 + // ^ | + // |---------[3-1]---------------| + + const node1Id = await Engine.insertNode(b1, modelId, "Node1", "op1", 1, new Point3d(0, 0, 0)); + const node2Id = await Engine.insertNode(b1, modelId, "Node2", "op2", 2, new Point3d(1, 1, 1)); + const node3Id = await Engine.insertNode(b1, modelId, "Node3", "op3", 3, new Point3d(2, 2, 2)); + + const node12 = await Engine.insertEdge(b1, node1Id, node2Id, "1-2"); + const node23 = await Engine.insertEdge(b1, node2Id, node3Id, "2-3"); + const node31 = await Engine.insertEdge(b1, node3Id, node1Id, "3-1"); + + chai.expect(node1Id).to.be.equals("0x20000000004"); + chai.expect(node2Id).to.be.equals("0x20000000005"); + chai.expect(node3Id).to.be.equals("0x20000000006"); + chai.expect(node12).to.be.equals("0x20000000001"); + chai.expect(node23).to.be.equals("0x20000000002"); + chai.expect(node31).to.be.equals("0x20000000003"); + + b1.saveChanges(); + }); +}); \ No newline at end of file diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 7213d107d5f..44a9b2df0a3 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -3,14 +3,14 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +import { Guid, Id64String } from "@itwin/core-bentley"; +import { Code, GeometricElement2dProps, IModel, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; +import { Suite } from "mocha"; import { HubWrappers, IModelTestUtils, KnownTestLocations } from ".."; import { BriefcaseDb, BriefcaseManager, ChannelControl, DrawingCategory, IModelHost, SqliteChangesetReader, TxnProps } from "../../core-backend"; import { HubMock } from "../../internal/HubMock"; -import { Suite } from "mocha"; -import { Code, GeometricElement2dProps, IModel, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; -import { Guid, Id64String } from "@itwin/core-bentley"; import { StashManager } from "../../StashManager"; chai.use(chaiAsPromised); @@ -724,64 +724,5 @@ describe("rebase changes & stashing api", function (this: Suite) { await chai.expect(b2.pushChanges({ description: `deleted child ${childId} and inserted grandchild ${grandChildId}` })) .to.be.rejectedWith("Foreign key conflicts in ChangeSet. Aborting rebase."); }); - it.only("EDE", async () => { - const schema1 = ` - - - - bis:GraphicalElement2d - - - - - - bis:ElementOwnsChildElements - - - - - - - - `; - - const b1 = await testIModel.openBriefcase(); - await b1.importSchemaStrings([schema1]); - const model = testIModel.drawingModelId; - const category = testIModel.drawingCategoryId; - const code = Code.createEmpty(); - const n1 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, - op: "", var: "n1", val: 100 - } as NodeElementProps); - chai.expect(n1).to.exist; - const n2 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, - op: "", var: "n2", val: 200 - } as NodeElementProps); - chai.expect(n2).to.exist; - const n3 = b1.elements.insertElement({ classFullName: "EDE:Node", model, category, code, - op: "sum", var: "n3", val: 0 - } as NodeElementProps); - chai.expect(n3).to.exist; - - b1.relationships.insertInstance({ - classFullName: "EDE:InputDrivesOutput", - sourceId: n1, - targetId: n3, - }); - - b1.relationships.insertInstance({ - classFullName: "EDE:InputDrivesOutput", - sourceId: n2, - targetId: n3, - }); - - b1.txns.on - }); }); - -interface NodeElementProps extends GeometricElement2dProps { - op: string; - var: string; - val: number; -} From b361b2917f68994d7c3f938304089f6f13a5eb70 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 25 Sep 2025 14:21:38 -0400 Subject: [PATCH 03/17] fix --- core/backend/src/test/EDE.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index 85e58beaff2..f832757be9c 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -199,7 +199,7 @@ describe.only("EDE Tests", () => { HubMock.shutdown(); }); - it("should insert and count nodes", async () => { + it("cyclical graph creation and validation", async () => { const b1 = await openBriefcase(); const { modelId, categoryId } = await Engine.initialize(b1); From 394f707d67809435b5fc6ba9e20d1f24c9c270c9 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Mon, 29 Sep 2025 13:36:57 -0400 Subject: [PATCH 04/17] WIP --- core/backend/src/test/EDE.test.ts | 518 ++++++++++++++++++++++++++++-- 1 file changed, 486 insertions(+), 32 deletions(-) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index f832757be9c..e15450ffd48 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -1,5 +1,5 @@ -import { DbResult, Id64String, IModelStatus } from "@itwin/core-bentley"; -import { Code, ElementProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; +import { BeEvent, DbResult, Id64String, IModelStatus } from "@itwin/core-bentley"; +import { Code, ElementProps, EntityProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; @@ -15,7 +15,239 @@ import { ElementDrivesElement, ElementDrivesElementProps } from "../Relationship import { Schema, Schemas } from "../Schema"; import { HubWrappers } from "./IModelTestUtils"; import { KnownTestLocations } from "./KnownTestLocations"; +import { on } from "node:events"; chai.use(chaiAsPromised); +/** + 1. What is Change Propagation?** + In engineering, models often consist of many interdependent components (e.g., parts, assemblies, constraints). When you modify one component (say, changing a dimension), that change can affect other components. + **Change propagation** is the process of updating all dependent components so the design remains consistent. + + 2. Why Use Topological Sort?** + The dependencies between components can be represented as a **Directed Acyclic Graph (DAG)**: + - **Nodes** = components or features. + - **Edges** = dependency relationships (e.g., "Feature B depends on Feature A"). + + To propagate changes correctly: + - You must update components **in dependency order** (parents before children). + - This is where **topological sorting** comes in—it gives a linear order of nodes such that every dependency comes before the dependent. + + 3. How It Works** + **Steps:** + 1. **Build the dependency graph**: + - For each feature/component, list what it depends on. + 2. **Perform topological sort**: + - Use algorithms like **Kahn’s Algorithm** or **DFS-based sort**. + 3. **Propagate changes in sorted order**: + - Start from nodes with no dependencies (roots). + - Update each node, then move to its dependents. + + + 4. Example** + Imagine a CAD model: + - **Sketch → Extrude → Fillet → Hole** + - If you change the **Sketch**, the **Extrude**, **Fillet**, and **Hole** must update in that order. + + Graph: + Sketch → Extrude → Fillet → Hole + Topological sort result: + [Sketch, Extrude, Fillet, Hole] + Update in this order to maintain consistency. + + 5. Benefits** + - Prevents circular updates (since DAG ensures no cycles). + - Ensures deterministic and efficient update propagation. + - Scales well for complex assemblies. + */ + +enum Color { + White, // unvisited + Gray, // visiting + Black, // visited +} + +export class Graph { + private _nodes: T[] = []; + private _edges = new Map(); + public constructor() { + } + + public addNode(node: T): void { + if (!this._nodes.includes(node)) + this._nodes.push(node); + } + + public *nodes(): IterableIterator { + yield* this._nodes; + } + + public *edges(): IterableIterator<{ from: T, to: T }> { + for (const [from, toList] of this._edges.entries()) { + for (const to of toList) { + yield { from, to }; + } + } + } + + public addEdge(from: T, to: T | T[]): void { + this.addNode(from); + if (!this._edges.has(from)) { + this._edges.set(from, []); + } + if (Array.isArray(to)) { + to.forEach(t => this.addNode(t)); + this._edges.get(from)!.push(...to); + } else { + this.addNode(to); + this._edges.get(from)!.push(to); + } + } + public getEdges(node: T): T[] { + if (!this._edges.has(node)) + return []; + return this._edges.get(node)!; + } + public clone(): Graph { + const newGraph = new Graph(); + for (const node of this._nodes) { + newGraph.addNode(node); + } + for (const [from, toList] of this._edges.entries()) { + newGraph.addEdge(from, toList); + } + return newGraph; + } + public toGraphvis(accessor: NodeAccessor): string { + // Implementation for converting the graph to Graphviz DOT format + let dot = "digraph G {\n"; + for (const node of this._nodes) { + dot += ` "${accessor.getId(node)}" [label="${accessor.getLabel(node)}"];\n`; + } + for (const [from, toList] of this._edges.entries()) { + for (const to of toList) { + dot += ` "${accessor.getId(from)}" -> "${accessor.getId(to)}";\n`; + } + } + dot += "}\n"; + return dot; + } +} +export interface NodeAccessor { + getLabel: (node: T) => string; + getId: (node: T) => string; +} +export class TopologicalSorter { + private static visit(graph: Graph, node: T, colors: Map, sorted: T[], failOnCycles: boolean): void { + if (colors.get(node) === Color.Gray) { + if (failOnCycles) + throw new Error("Graph has a cycle"); + else { + return; + } + } + + if (colors.get(node) === Color.White) { + colors.set(node, Color.Gray); + const neighbors = graph.getEdges(node); + for (const neighbor of neighbors) { + this.visit(graph, neighbor, colors, sorted, failOnCycles); + } + colors.set(node, Color.Black); + sorted.push(node); + } + } + + public static sortDepthFirst(graph: Graph, updated?: T[], failOnCycles = true): T[] { + const sorted: T[] = []; + const colors = new Map(Array.from(graph.nodes()).map((node) => [node, Color.White])); + if (updated) { + // remove duplicate + let filteredUpdated = Array.from(new Set(updated)); + filteredUpdated = filteredUpdated.filter(node => colors.get(node) === Color.White); + if (filteredUpdated.length !== updated.length) { + throw new Error("Updated list contains nodes that are not in the graph or have duplicates"); + } + if (filteredUpdated.length === 0) + updated = undefined; + else + updated = filteredUpdated; + } + for (const node of updated ?? Array.from(graph.nodes())) { + if (colors.get(node) === Color.White) { + this.visit(graph, node, colors, sorted, failOnCycles); + } + } + + return sorted.reverse(); + } + public static sortBreadthFirst(graph: Graph, updated?: T[], failOnCycles = true): T[] { + const sorted: T[] = []; + const queue: T[] = []; + // Vector to store indegree of each vertex + const inDegree = new Map(); + for (const node of graph.nodes()) { + inDegree.set(node, 0); + } + for (const { from, to } of graph.edges()) { + inDegree.set(to, (inDegree.get(to) ?? 0) + 1); + } + + if (updated) { + // remove duplicate + const filteredUpdated = Array.from(new Set(updated)); + if (filteredUpdated.length !== updated.length) { + throw new Error("Updated list contains nodes that are not in the graph or have duplicates"); + } + if (filteredUpdated.length === 0) + updated = undefined; + else + updated = filteredUpdated; + } + const startNodes = updated ?? Array.from(graph.nodes()); + for (const node of startNodes) { + if (inDegree.get(node) === 0) { + queue.push(node); + } + } + if (startNodes.length === 0) { + throw new Error("Graph has at least one cycle"); + } + + if (startNodes) + while (queue.length > 0) { + const current = queue.shift()!; + sorted.push(current); + + for (const neighbor of graph.getEdges(current)) { + inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1); + if (inDegree.get(neighbor) === 0) { + queue.push(neighbor); + } + } + } + if (failOnCycles && sorted.length !== Array.from(graph.nodes()).length) + throw new Error("Graph has at least one cycle"); + + return sorted; + } + public static validate(graph: Graph, sorted: T[]): boolean { + if (sorted.length !== Array.from(graph.nodes()).length) { + return false; + } + + const position = new Map(); + for (let i = 0; i < sorted.length; i++) { + position.set(sorted[i], i); + } + + for (const { from, to } of graph.edges()) { + if (position.get(from)! > position.get(to)!) { + return false; + } + } + return true; + } +} + export interface InputDrivesOutputProps extends ElementDrivesElementProps { prop: string @@ -27,17 +259,29 @@ export interface NodeElementProps extends GeometricElement3dProps { } export class InputDrivesOutput extends ElementDrivesElement { + public static readonly events = { + onRootChanged: new BeEvent<(props: RelationshipProps, iModel: IModelDb) => void>(), + onDeletedDependency: new BeEvent<(props: RelationshipProps, iModel: IModelDb) => void>(), + }; public static override get className(): string { return "InputDrivesOutput"; } protected constructor(props: InputDrivesOutputProps, iModel: IModelDb) { super(props, iModel); } - public static override onRootChanged(_props: RelationshipProps, _iModel: IModelDb): void { } - public static override onDeletedDependency(_props: RelationshipProps, _iModel: IModelDb): void { } + public static override onRootChanged(props: RelationshipProps, iModel: IModelDb): void { + this.events.onRootChanged.raiseEvent(props, iModel); + } + public static override onDeletedDependency(props: RelationshipProps, iModel: IModelDb): void { + this.events.onDeletedDependency.raiseEvent(props, iModel); + } } - export class NodeElement extends GeometricElement3d { public op: string; public val: number; + public static readonly events = { + onAllInputsHandled: new BeEvent<(id: Id64String, iModel: IModelDb) => void>(), + onBeforeOutputsHandled: new BeEvent<(id: Id64String, iModel: IModelDb) => void>(), + }; + public static override get className(): string { return "Node"; } protected constructor(props: NodeElementProps, iModel: IModelDb) { super(props, iModel); @@ -50,8 +294,12 @@ export class NodeElement extends GeometricElement3d { val.val = this.val; return val; } - protected static override onAllInputsHandled(_id: Id64String, _iModel: IModelDb): void { } - protected static override onBeforeOutputsHandled(_id: Id64String, _iModel: IModelDb): void { } + protected static override onAllInputsHandled(id: Id64String, iModel: IModelDb): void { + this.events.onAllInputsHandled.raiseEvent(id, iModel); + } + protected static override onBeforeOutputsHandled(id: Id64String, iModel: IModelDb): void { + this.events.onBeforeOutputsHandled.raiseEvent(id, iModel); + } public static generateGeometry(radius: number): GeometryStreamProps { const builder = new GeometryStreamBuilder(); const p1 = Point3d.createZero(); @@ -106,14 +354,73 @@ export class NetworkSchema extends Schema { } export class Engine { + public static async createGraph(iModelDb: IModelDb, modelId: Id64String, graph: Graph): Promise> { + const nodes = new Map(); + const outGraph = new Graph<{ id: Id64String, name: string }>(); + for (const node of graph.nodes()) { + const id = await this.insertNode(iModelDb, modelId, node, "", 0, new Point3d(0, 0, 0)); + nodes.set(node, { id, name: node }); + } + for (const edge of graph.edges()) { + const fromId = nodes.get(edge.from)!.id; + const toId = nodes.get(edge.to)!.id; + await this.insertEdge(iModelDb, fromId, toId, ""); + outGraph.addEdge(nodes.get(edge.from)!, nodes.get(edge.to)!); + } + return outGraph; + } public static countNodes(iModelDb: IModelDb): number { - return iModelDb.withSqliteStatement("SELECT COUNT(*) FROM Network.Node", (stmt) => { + return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM Network.Node", (stmt) => { if (stmt.step() === DbResult.BE_SQLITE_ROW) { return stmt.getValue(0).getInteger(); } return 0; }); } + public static countEdges(iModelDb: IModelDb): number { + return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM Network.InputDrivesOutput", (stmt) => { + if (stmt.step() === DbResult.BE_SQLITE_ROW) { + return stmt.getValue(0).getInteger(); + } + return 0; + }); + } + public static queryEdgesForSource(iModelDb: IModelDb, sourceId: Id64String): InputDrivesOutputProps[] { + const edges: InputDrivesOutputProps[] = []; + iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [SourceECInstanceId] = ?", (stmt) => { + stmt.bindId(1, sourceId); + while (stmt.step() === DbResult.BE_SQLITE_ROW) { + edges.push({ + id: stmt.getValue(0).getId(), + classFullName: InputDrivesOutput.classFullName, + sourceId: stmt.getValue(1).getId(), + targetId: stmt.getValue(2).getId(), + prop: stmt.getValue(3).getString(), + status: stmt.getValue(4).getInteger(), + priority: stmt.getValue(5).getInteger(), + }); + } + }); + return edges; + } + public static queryEdgesForTarget(iModelDb: IModelDb, targetId: Id64String): InputDrivesOutputProps[] { + const edges: InputDrivesOutputProps[] = []; + iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [TargetECInstanceId] = ?", (stmt) => { + stmt.bindId(1, targetId); + while (stmt.step() === DbResult.BE_SQLITE_ROW) { + edges.push({ + id: stmt.getValue(0).getId(), + classFullName: InputDrivesOutput.classFullName, + sourceId: stmt.getValue(1).getId(), + targetId: stmt.getValue(2).getId(), + prop: stmt.getValue(3).getString(), + status: stmt.getValue(4).getInteger(), + priority: stmt.getValue(5).getInteger(), + }); + } + }); + return edges; + } private static async createPartition(iModelDb: IModelDb): Promise { const parentId = new SubjectOwnsPartitionElements(IModel.rootSubjectId); const modelId = IModel.repositoryModelId; @@ -149,7 +456,6 @@ export class Engine { categoryId, }; } - public static async insertNode(iModelDb: IModelDb, modelId: Id64String, name: string, op: string, val: number, location: Point3d, radius: number = 0.1) { const props: NodeElementProps = { classFullName: NodeElement.classFullName, @@ -165,6 +471,26 @@ export class Engine { await iModelDb.locks.acquireLocks({ shared: modelId }); return iModelDb.elements.insertElement(props); } + public static async deleteNode(iModelDb: IModelDb, nodeId: Id64String) { + await iModelDb.locks.acquireLocks({ exclusive: nodeId }); + return iModelDb.elements.deleteElement(nodeId); + } + public static async updateNode(iModelDb: IModelDb, props: Partial) { + await iModelDb.locks.acquireLocks({ exclusive: props.id }); + return iModelDb.elements.updateElement(props); + } + public static async updateNodeWithName(iModelDb: IModelDb, userLabel: string) { + const id = iModelDb.withPreparedStatement("SELECT [ECInstanceId] FROM [Network].[Node] WHERE [UserLabel] = ?", (stmt) => { + stmt.bindString(1, userLabel); + if (stmt.step() === DbResult.BE_SQLITE_ROW) + return stmt.getValue(0).getId(); + return undefined; + }); + if (!id) { + throw new Error(`Node with userLabel ${userLabel} not found`); + } + await this.updateNode(iModelDb, { id }); + } public static async insertEdge(iModelDb: IModelDb, sourceId: Id64String, targetId: Id64String, prop: string) { const props: InputDrivesOutputProps = { classFullName: InputDrivesOutput.classFullName, @@ -176,6 +502,10 @@ export class Engine { }; return iModelDb.relationships.insertInstance(props); } + public static async deleteEdge(iModelDb: IModelDb, edgeId: Id64String) { + const edge = iModelDb.relationships.getInstanceProps(InputDrivesOutput.classFullName, edgeId); + return iModelDb.relationships.deleteInstance(edge); + } } describe.only("EDE Tests", () => { @@ -193,41 +523,165 @@ describe.only("EDE Tests", () => { iModelId = await HubMock.createNewIModel({ iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" }); }); afterEach(async () => { + NodeElement.events.onAllInputsHandled.clear(); + NodeElement.events.onBeforeOutputsHandled.clear(); + InputDrivesOutput.events.onRootChanged.clear(); + InputDrivesOutput.events.onDeletedDependency.clear(); for (const briefcase of briefcases) { briefcase.close(); } HubMock.shutdown(); }); + it("local: topological sort", async () => { + const graph = new Graph(); + // Graph structure: + // 1 + // / \ + // 2 3 + // |\ \ + // | \ \ + // 4 5 4 + // \ / + // \ / + // 5 - it("cyclical graph creation and validation", async () => { - const b1 = await openBriefcase(); - const { modelId, categoryId } = await Engine.initialize(b1); + graph.addEdge("1", ["2", "3"]); + graph.addEdge("2", ["5", "4"]); + graph.addEdge("3", ["4"]); + graph.addEdge("4", ["5"]); + const df = TopologicalSorter.sortDepthFirst(graph); + chai.expect(TopologicalSorter.validate(graph, df)).to.be.true; + chai.expect(df).to.deep.equal(["1", "3", "2", "4", "5"]); - chai.expect(modelId).to.equals("0x20000000001"); - chai.expect(categoryId).to.equals("0x20000000002"); + const bf = TopologicalSorter.sortBreadthFirst(graph); + chai.expect(TopologicalSorter.validate(graph, bf)).to.be.true; + chai.expect(bf).to.deep.equal(["1", "2", "3", "4", "5"]); + }); - b1.saveChanges(); - await b1.pullChanges(); + it("local: cycle detection (suppress cycles)", async () => { + const graph = new Graph(); + + // Graph structure: + // 1 --> 2 --> 3 + // ^ | + // |-----------| + graph.addEdge("1", ["2"]); + graph.addEdge("2", ["3"]); + graph.addEdge("3", ["1"]); + + const df = TopologicalSorter.sortDepthFirst(graph, [], false); + chai.expect(TopologicalSorter.validate(graph, df)).to.be.false; + chai.expect(df).to.deep.equal(["1", "2", "3"]); + + // const bf = TopologicalSorter.sortBreadthFirst(graph, [], false); + // chai.expect(TopologicalSorter.validate(graph, bf)).to.be.false; + // chai.expect(bf).to.deep.equal(["1", "2", "3"]); + }); + it("local: cycle detection (throw)", async () => { + const graph = new Graph(); + // Graph structure: + // 1 --> 2 --> 3 + // ^ | + // |-----------| + graph.addEdge("1", ["2"]); + graph.addEdge("2", ["3"]); + graph.addEdge("3", ["1"]); + + chai.expect(() => TopologicalSorter.sortDepthFirst(graph)).to.throw( + "Graph has a cycle" + ); + }); + it("local: complex, subset", async () => { + const graph = new Graph(); + + graph.addEdge("Socks", ["Shoes"]); + graph.addEdge("Underwear", ["Shoes", "Pants"]); + graph.addEdge("Pants", ["Belt", "Shoes"]); + graph.addEdge("Shirt", ["Belt", "Tie"]); + graph.addEdge("Tie", ["Jacket"]); + graph.addEdge("Belt", ["Jacket"]); + graph.addNode("Watch"); + + const sorted = TopologicalSorter.sortDepthFirst(graph); + chai.expect(TopologicalSorter.validate(graph, sorted)).to.be.true; + chai.expect(sorted).to.deep.equal(["Watch", "Shirt", "Tie", "Underwear", "Pants", "Belt", "Jacket", "Socks", "Shoes"]); + + // Test sorting with a subset of nodes + const sorted1 = TopologicalSorter.sortDepthFirst(graph, ["Underwear"]); + chai.expect(sorted1).to.deep.equal(["Underwear", "Pants", "Belt", "Jacket", "Shoes"]); + + const sorted2 = TopologicalSorter.sortDepthFirst(graph, ["Belt"]); + chai.expect(sorted2).to.deep.equal(["Belt", "Jacket"]); - // Node1 ---[1-2]---> Node2 ---[2-3]---> Node3 - // ^ | - // |---------[3-1]---------------| + const sorted3 = TopologicalSorter.sortDepthFirst(graph, ["Shoes"]); + chai.expect(sorted3).to.deep.equal(["Shoes"]); - const node1Id = await Engine.insertNode(b1, modelId, "Node1", "op1", 1, new Point3d(0, 0, 0)); - const node2Id = await Engine.insertNode(b1, modelId, "Node2", "op2", 2, new Point3d(1, 1, 1)); - const node3Id = await Engine.insertNode(b1, modelId, "Node3", "op3", 3, new Point3d(2, 2, 2)); + const sorted4 = TopologicalSorter.sortDepthFirst(graph, ["Socks"]); + chai.expect(sorted4).to.deep.equal(["Socks", "Shoes"]); - const node12 = await Engine.insertEdge(b1, node1Id, node2Id, "1-2"); - const node23 = await Engine.insertEdge(b1, node2Id, node3Id, "2-3"); - const node31 = await Engine.insertEdge(b1, node3Id, node1Id, "3-1"); + const sorted5 = TopologicalSorter.sortDepthFirst(graph, ["Tie"]); + chai.expect(sorted5).to.deep.equal(["Tie", "Jacket"]); - chai.expect(node1Id).to.be.equals("0x20000000004"); - chai.expect(node2Id).to.be.equals("0x20000000005"); - chai.expect(node3Id).to.be.equals("0x20000000006"); - chai.expect(node12).to.be.equals("0x20000000001"); - chai.expect(node23).to.be.equals("0x20000000002"); - chai.expect(node31).to.be.equals("0x20000000003"); + const sorted6 = TopologicalSorter.sortDepthFirst(graph, ["Jacket"]); + chai.expect(sorted6).to.deep.equal(["Jacket"]); + + const sorted7 = TopologicalSorter.sortDepthFirst(graph, ["Shirt"]); + chai.expect(sorted7).to.deep.equal(["Shirt", "Tie", "Belt", "Jacket"]); + + const sorted8 = TopologicalSorter.sortDepthFirst(graph, ["Watch"]); + chai.expect(sorted8).to.deep.equal(["Watch"]); + }); + + it.only("EDE: basic graph operations", async () => { + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const graph = new Graph(); + + graph.addEdge("1", ["2", "3"]); + graph.addEdge("2", ["5", "4"]); + graph.addEdge("3", ["4"]); + graph.addEdge("4", ["5"]); + + await Engine.createGraph(b1, modelId, graph); + const onRootChanged: [string, string][] = []; + const onAllInputsHandled: string[] = []; + const onBeforeOutputsHandled: string[] = []; + const onDeletedDependency: [string, string][] = []; + + InputDrivesOutput.events.onDeletedDependency.addListener((props: RelationshipProps) => onDeletedDependency.push([b1.elements.tryGetElement(props.sourceId)?.userLabel as string, b1.elements.tryGetElement(props.targetId)?.userLabel as string])); + InputDrivesOutput.events.onRootChanged.addListener((props: RelationshipProps) => onRootChanged.push([b1.elements.tryGetElement(props.sourceId)?.userLabel as string, b1.elements.tryGetElement(props.targetId)?.userLabel as string])); + NodeElement.events.onAllInputsHandled.addListener((id: Id64String) => onAllInputsHandled.push(b1.elements.tryGetElement(id)?.userLabel as string)); + NodeElement.events.onBeforeOutputsHandled.addListener((id: Id64String) => onBeforeOutputsHandled.push(b1.elements.tryGetElement(id)?.userLabel as string)); b1.saveChanges(); + chai.expect(onRootChanged).to.deep.equal([ + ["1", "2"], + ["1", "3"], + ["2", "5"], + ["2", "4"], + ["3", "4"], + ["4", "5"]]); + chai.expect(onAllInputsHandled).to.deep.equal(["2", "3", "4", "5"]); + chai.expect(onBeforeOutputsHandled).to.deep.equal(["1"]); + chai.expect(onDeletedDependency).to.deep.equal([]); + + onRootChanged.length = 0; + onAllInputsHandled.length = 0; + onBeforeOutputsHandled.length = 0; + onDeletedDependency.length = 0; + + await Engine.updateNodeWithName(b1, "2"); + b1.saveChanges(); + + chai.expect(onRootChanged).to.deep.equal([ + ["2", "5"], + ["2", "4"], + ["4", "5"]]); + chai.expect(onAllInputsHandled).to.deep.equal(["4", "5"]); + chai.expect(onBeforeOutputsHandled).to.deep.equal(["2"]); + chai.expect(onDeletedDependency).to.deep.equal([]); + }); -}); \ No newline at end of file +}); + + From f53a47ee432eb48a259d6a525b3a70105cb56326 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 30 Sep 2025 10:52:53 -0400 Subject: [PATCH 05/17] Improve test --- core/backend/src/test/EDE.test.ts | 138 +++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 42 deletions(-) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index e15450ffd48..656a5014b34 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -248,7 +248,24 @@ export class TopologicalSorter { } } - +class ElementDrivesElementEventMonitor { + public readonly onRootChanged: [string, string][] = []; + public readonly onAllInputsHandled: string[] = []; + public readonly onBeforeOutputsHandled: string[] = []; + public readonly onDeletedDependency: [string, string][] = []; + constructor(public iModelDb: IModelDb) { + InputDrivesOutput.events.onDeletedDependency.addListener((props: RelationshipProps) => this.onDeletedDependency.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel as string, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel as string])); + InputDrivesOutput.events.onRootChanged.addListener((props: RelationshipProps) => this.onRootChanged.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel as string, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel as string])); + NodeElement.events.onAllInputsHandled.addListener((id: Id64String) => this.onAllInputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel as string)); + NodeElement.events.onBeforeOutputsHandled.addListener((id: Id64String) => this.onBeforeOutputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel as string)); + } + public clear() { + this.onRootChanged.length = 0; + this.onAllInputsHandled.length = 0; + this.onBeforeOutputsHandled.length = 0; + this.onDeletedDependency.length = 0; + } +} export interface InputDrivesOutputProps extends ElementDrivesElementProps { prop: string } @@ -560,9 +577,8 @@ describe.only("EDE Tests", () => { it("local: cycle detection (suppress cycles)", async () => { const graph = new Graph(); - // Graph structure: - // 1 --> 2 --> 3 + // 1 --> 2 <-- 3 // ^ | // |-----------| graph.addEdge("1", ["2"]); @@ -580,6 +596,8 @@ describe.only("EDE Tests", () => { it("local: cycle detection (throw)", async () => { const graph = new Graph(); // Graph structure: + // A---B---C + //. \ D / // 1 --> 2 --> 3 // ^ | // |-----------| @@ -591,6 +609,45 @@ describe.only("EDE Tests", () => { "Graph has a cycle" ); }); + it("local: build system dependencies", async () => { + const graph = new Graph(); + /* + Example: Build system dependencies + + - Compile main.c and util.c to main.o and util.o + - Link main.o and util.o to produce app.exe + - app.exe depends on config.json + - test.exe depends on main.o, util.o, and test.c + + Graph: + main.c util.c test.c config.json + | | | | + v v | | + main.o util.o | | + \ / | | + \ / | | + app.exe test.exe | + | | | + +----------------+---------+ + */ + + graph.addEdge("main.c", ["main.o"]); + graph.addEdge("util.c", ["util.o"]); + graph.addEdge("test.c", ["test.exe"]); + graph.addEdge("main.o", ["app.exe", "test.exe"]); + graph.addEdge("util.o", ["app.exe", "test.exe"]); + graph.addEdge("config.json", ["app.exe"]); + + // Topological sort (depth-first) + const df = TopologicalSorter.sortDepthFirst(graph); + chai.expect(TopologicalSorter.validate(graph, df)).to.be.true; + + // Topological sort (breadth-first) + const bf = TopologicalSorter.sortBreadthFirst(graph); + chai.expect(TopologicalSorter.validate(graph, bf)).to.be.true; + chai.expect(df).to.deep.equal(["config.json", "test.c", "util.c", "util.o", "main.c", "main.o", "test.exe", "app.exe"]); + chai.expect(bf).to.deep.equal(["main.c", "util.c", "test.c", "config.json", "main.o", "util.o", "app.exe", "test.exe"]); + }); it("local: complex, subset", async () => { const graph = new Graph(); @@ -637,50 +694,47 @@ describe.only("EDE Tests", () => { const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); - graph.addEdge("1", ["2", "3"]); - graph.addEdge("2", ["5", "4"]); - graph.addEdge("3", ["4"]); - graph.addEdge("4", ["5"]); - + // Graph structure: + // A + // / \ + // B C + // |\ \ + // | \ \ + // E D D + // \ / + // \ / + // E + graph.addEdge("A", ["B", "C"]); + graph.addEdge("B", ["E", "D"]); + graph.addEdge("C", ["D"]); + graph.addEdge("D", ["E"]); await Engine.createGraph(b1, modelId, graph); - const onRootChanged: [string, string][] = []; - const onAllInputsHandled: string[] = []; - const onBeforeOutputsHandled: string[] = []; - const onDeletedDependency: [string, string][] = []; - - InputDrivesOutput.events.onDeletedDependency.addListener((props: RelationshipProps) => onDeletedDependency.push([b1.elements.tryGetElement(props.sourceId)?.userLabel as string, b1.elements.tryGetElement(props.targetId)?.userLabel as string])); - InputDrivesOutput.events.onRootChanged.addListener((props: RelationshipProps) => onRootChanged.push([b1.elements.tryGetElement(props.sourceId)?.userLabel as string, b1.elements.tryGetElement(props.targetId)?.userLabel as string])); - NodeElement.events.onAllInputsHandled.addListener((id: Id64String) => onAllInputsHandled.push(b1.elements.tryGetElement(id)?.userLabel as string)); - NodeElement.events.onBeforeOutputsHandled.addListener((id: Id64String) => onBeforeOutputsHandled.push(b1.elements.tryGetElement(id)?.userLabel as string)); + const monitor = new ElementDrivesElementEventMonitor(b1); b1.saveChanges(); - chai.expect(onRootChanged).to.deep.equal([ - ["1", "2"], - ["1", "3"], - ["2", "5"], - ["2", "4"], - ["3", "4"], - ["4", "5"]]); - chai.expect(onAllInputsHandled).to.deep.equal(["2", "3", "4", "5"]); - chai.expect(onBeforeOutputsHandled).to.deep.equal(["1"]); - chai.expect(onDeletedDependency).to.deep.equal([]); - - onRootChanged.length = 0; - onAllInputsHandled.length = 0; - onBeforeOutputsHandled.length = 0; - onDeletedDependency.length = 0; - - await Engine.updateNodeWithName(b1, "2"); + chai.expect(monitor.onRootChanged).to.deep.equal([ + ["A", "B"], + ["A", "C"], + ["B", "E"], + ["B", "D"], + ["C", "D"], + ["D", "E"]]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["B", "C", "D", "E"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["A"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + + monitor.clear(); + + await Engine.updateNodeWithName(b1, "B"); b1.saveChanges(); - chai.expect(onRootChanged).to.deep.equal([ - ["2", "5"], - ["2", "4"], - ["4", "5"]]); - chai.expect(onAllInputsHandled).to.deep.equal(["4", "5"]); - chai.expect(onBeforeOutputsHandled).to.deep.equal(["2"]); - chai.expect(onDeletedDependency).to.deep.equal([]); - + chai.expect(monitor.onRootChanged).to.deep.equal([ + ["B", "E"], + ["B", "D"], + ["D", "E"]]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["D", "E"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["B"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); }); }); From 3272184a5609c5318e85d52fae2200e38fe698eb Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 30 Sep 2025 11:07:37 -0400 Subject: [PATCH 06/17] improve test --- core/backend/src/test/EDE.test.ts | 60 +++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index 656a5014b34..464795d912e 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -387,6 +387,7 @@ export class Engine { return outGraph; } public static countNodes(iModelDb: IModelDb): number { + // eslint-disable-next-line @typescript-eslint/no-deprecated return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM Network.Node", (stmt) => { if (stmt.step() === DbResult.BE_SQLITE_ROW) { return stmt.getValue(0).getInteger(); @@ -395,7 +396,8 @@ export class Engine { }); } public static countEdges(iModelDb: IModelDb): number { - return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM Network.InputDrivesOutput", (stmt) => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM [Network].[InputDrivesOutput]", (stmt) => { if (stmt.step() === DbResult.BE_SQLITE_ROW) { return stmt.getValue(0).getInteger(); } @@ -404,6 +406,7 @@ export class Engine { } public static queryEdgesForSource(iModelDb: IModelDb, sourceId: Id64String): InputDrivesOutputProps[] { const edges: InputDrivesOutputProps[] = []; + // eslint-disable-next-line @typescript-eslint/no-deprecated iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [SourceECInstanceId] = ?", (stmt) => { stmt.bindId(1, sourceId); while (stmt.step() === DbResult.BE_SQLITE_ROW) { @@ -422,6 +425,7 @@ export class Engine { } public static queryEdgesForTarget(iModelDb: IModelDb, targetId: Id64String): InputDrivesOutputProps[] { const edges: InputDrivesOutputProps[] = []; + // eslint-disable-next-line @typescript-eslint/no-deprecated iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [TargetECInstanceId] = ?", (stmt) => { stmt.bindId(1, targetId); while (stmt.step() === DbResult.BE_SQLITE_ROW) { @@ -492,11 +496,12 @@ export class Engine { await iModelDb.locks.acquireLocks({ exclusive: nodeId }); return iModelDb.elements.deleteElement(nodeId); } - public static async updateNode(iModelDb: IModelDb, props: Partial) { + public static async updateNodeProps(iModelDb: IModelDb, props: Partial) { await iModelDb.locks.acquireLocks({ exclusive: props.id }); return iModelDb.elements.updateElement(props); } - public static async updateNodeWithName(iModelDb: IModelDb, userLabel: string) { + public static async updateNode(iModelDb: IModelDb, userLabel: string) { + // eslint-disable-next-line @typescript-eslint/no-deprecated const id = iModelDb.withPreparedStatement("SELECT [ECInstanceId] FROM [Network].[Node] WHERE [UserLabel] = ?", (stmt) => { stmt.bindString(1, userLabel); if (stmt.step() === DbResult.BE_SQLITE_ROW) @@ -506,7 +511,31 @@ export class Engine { if (!id) { throw new Error(`Node with userLabel ${userLabel} not found`); } - await this.updateNode(iModelDb, { id }); + await this.updateNodeProps(iModelDb, { id }); + } + public static async deleteEdge(iModelDb: IModelDb, from: string, to: string) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + const edge = iModelDb.withPreparedStatement(` + SELECT [IDo].[ECInstanceId], [IDo].[SourceECInstanceId], [IDo].[TargetECInstanceId] + FROM [Network].[InputDrivesOutput] [IDo] + JOIN [Network].[Node] [Src] ON [Src].[ECInstanceId] = [IDo].[SourceECInstanceId] + JOIN [Network].[Node] [Tgt] ON [Tgt].[ECInstanceId] = [IDo].[TargetECInstanceId] + WHERE [Src].[UserLabel] = ? AND [Tgt].[UserLabel] = ?`, (stmt) => { + stmt.bindString(1, from); + stmt.bindString(2, to); + if (stmt.step() === DbResult.BE_SQLITE_ROW) + return { + id: stmt.getValue(0).getId(), + classFullName: InputDrivesOutput.classFullName, + sourceId: stmt.getValue(1).getId(), + targetId: stmt.getValue(2).getId(), + } as RelationshipProps; + return undefined; + }); + if (!edge) { + throw new Error(`Edge from ${from} to ${to} not found`); + } + iModelDb.relationships.deleteInstance(edge); } public static async insertEdge(iModelDb: IModelDb, sourceId: Id64String, targetId: Id64String, prop: string) { const props: InputDrivesOutputProps = { @@ -519,10 +548,6 @@ export class Engine { }; return iModelDb.relationships.insertInstance(props); } - public static async deleteEdge(iModelDb: IModelDb, edgeId: Id64String) { - const edge = iModelDb.relationships.getInstanceProps(InputDrivesOutput.classFullName, edgeId); - return iModelDb.relationships.deleteInstance(edge); - } } describe.only("EDE Tests", () => { @@ -708,9 +733,10 @@ describe.only("EDE Tests", () => { graph.addEdge("B", ["E", "D"]); graph.addEdge("C", ["D"]); graph.addEdge("D", ["E"]); - await Engine.createGraph(b1, modelId, graph); const monitor = new ElementDrivesElementEventMonitor(b1); + // create a network + await Engine.createGraph(b1, modelId, graph); b1.saveChanges(); chai.expect(monitor.onRootChanged).to.deep.equal([ ["A", "B"], @@ -722,12 +748,11 @@ describe.only("EDE Tests", () => { chai.expect(monitor.onAllInputsHandled).to.deep.equal(["B", "C", "D", "E"]); chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["A"]); chai.expect(monitor.onDeletedDependency).to.deep.equal([]); - monitor.clear(); - await Engine.updateNodeWithName(b1, "B"); + // update a node in network + await Engine.updateNode(b1, "B"); b1.saveChanges(); - chai.expect(monitor.onRootChanged).to.deep.equal([ ["B", "E"], ["B", "D"], @@ -735,7 +760,14 @@ describe.only("EDE Tests", () => { chai.expect(monitor.onAllInputsHandled).to.deep.equal(["D", "E"]); chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["B"]); chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + monitor.clear(); + + // delete edge in network + await Engine.deleteEdge(b1, "B", "E"); + b1.saveChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal([]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([["B", "E"]]); }); }); - - From 43810e60adf8acd07800097ed07076fc46051c1f Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Fri, 10 Oct 2025 11:20:38 -0400 Subject: [PATCH 07/17] fix --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 99ba5b0b3a9..15681498ff2 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Guid, Id64String } from "@itwin/core-bentley"; -import { Code, GeometricElement2dProps, IModel, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; +import { Code, GeometricElement2dProps, IModel, QueryBinder, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { Suite } from "mocha"; From c556396514982b5dd00018e8044a0ee6e80cfbe3 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Fri, 10 Oct 2025 17:05:22 -0400 Subject: [PATCH 08/17] WIP --- core/backend/src/IModelDb.ts | 3 +++ core/backend/src/test/EDE.test.ts | 36 +++++++++++++++++++++++++------ core/bentley/src/BeSQLite.ts | 4 ++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts index bb9a281cb14..5898e09c230 100644 --- a/core/backend/src/IModelDb.ts +++ b/core/backend/src/IModelDb.ts @@ -853,6 +853,9 @@ export abstract class IModelDb extends IModel { } const stat = this[_nativeDb].saveChanges(args ? JSON.stringify(args) : undefined); + if (DbResult.BE_SQLITE_ERROR_PropagateChangesFailed === stat) + throw new IModelError(stat, `Could not save changes due to propagation failure.`); + if (DbResult.BE_SQLITE_OK !== stat) throw new IModelError(stat, `Could not save changes (${args?.description})`); } diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index 464795d912e..217b0d4cfc3 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -267,7 +267,7 @@ class ElementDrivesElementEventMonitor { } } export interface InputDrivesOutputProps extends ElementDrivesElementProps { - prop: string + prop: number; } export interface NodeElementProps extends GeometricElement3dProps { @@ -381,7 +381,7 @@ export class Engine { for (const edge of graph.edges()) { const fromId = nodes.get(edge.from)!.id; const toId = nodes.get(edge.to)!.id; - await this.insertEdge(iModelDb, fromId, toId, ""); + await this.insertEdge(iModelDb, fromId, toId, 0); outGraph.addEdge(nodes.get(edge.from)!, nodes.get(edge.to)!); } return outGraph; @@ -415,7 +415,7 @@ export class Engine { classFullName: InputDrivesOutput.classFullName, sourceId: stmt.getValue(1).getId(), targetId: stmt.getValue(2).getId(), - prop: stmt.getValue(3).getString(), + prop: stmt.getValue(3).getDouble(), status: stmt.getValue(4).getInteger(), priority: stmt.getValue(5).getInteger(), }); @@ -434,7 +434,7 @@ export class Engine { classFullName: InputDrivesOutput.classFullName, sourceId: stmt.getValue(1).getId(), targetId: stmt.getValue(2).getId(), - prop: stmt.getValue(3).getString(), + prop: stmt.getValue(3).getDouble(), status: stmt.getValue(4).getInteger(), priority: stmt.getValue(5).getInteger(), }); @@ -537,7 +537,7 @@ export class Engine { } iModelDb.relationships.deleteInstance(edge); } - public static async insertEdge(iModelDb: IModelDb, sourceId: Id64String, targetId: Id64String, prop: string) { + public static async insertEdge(iModelDb: IModelDb, sourceId: Id64String, targetId: Id64String, prop: number) { const props: InputDrivesOutputProps = { classFullName: InputDrivesOutput.classFullName, sourceId, @@ -714,7 +714,7 @@ describe.only("EDE Tests", () => { chai.expect(sorted8).to.deep.equal(["Watch"]); }); - it.only("EDE: basic graph operations", async () => { + it("EDE: basic graph operations", async () => { const b1 = await openBriefcase(); const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); @@ -770,4 +770,28 @@ describe.only("EDE Tests", () => { chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]); chai.expect(monitor.onDeletedDependency).to.deep.equal([["B", "E"]]); }); + it.only("EDE: cyclical throw exception", async () => { + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const graph = new Graph(); + // Graph structure with a cycle: + // A + // / \ + // B - C + + graph.addEdge("A", ["B"]); + graph.addEdge("B", ["C"]); + graph.addEdge("C", ["A"]); + + const monitor = new ElementDrivesElementEventMonitor(b1); + // create a network + await Engine.createGraph(b1, modelId, graph); + chai.expect(() => b1.saveChanges()).to.throw("Could not save changes due to propagation failure."); + b1.abandonChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([["B", "C"], ["C", "A"], ["A", "B"]]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["C", "A", "B"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + monitor.clear(); + }); }); diff --git a/core/bentley/src/BeSQLite.ts b/core/bentley/src/BeSQLite.ts index 4928e58046d..e82466351ee 100644 --- a/core/bentley/src/BeSQLite.ts +++ b/core/bentley/src/BeSQLite.ts @@ -160,6 +160,10 @@ export enum DbResult { BE_SQLITE_ERROR_SchemaUpgradeRecommended = (BE_SQLITE_IOERR | 22 << 24), /** schema update require data transform */ BE_SQLITE_ERROR_DataTransformRequired = (BE_SQLITE_IOERR | (23 << 24)), + /** Db not open */ + BE_SQLITE_ERROR_NOTOPEN = (BE_SQLITE_ERROR | (1<<24)), + /** Error propagating changes during commit */ + BE_SQLITE_ERROR_PropagateChangesFailed = (BE_SQLITE_ERROR | (2<<24)), BE_SQLITE_LOCKED_SHAREDCACHE = (BE_SQLITE_LOCKED | (1 << 8)), BE_SQLITE_BUSY_RECOVERY = (BE_SQLITE_BUSY | (1 << 8)), From bf88123150c47d1552b455e927d6a75141e334cd Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Fri, 10 Oct 2025 17:10:46 -0400 Subject: [PATCH 09/17] fix lint error --- core/backend/src/test/EDE.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index 217b0d4cfc3..f84447e2ce2 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -1,5 +1,5 @@ import { BeEvent, DbResult, Id64String, IModelStatus } from "@itwin/core-bentley"; -import { Code, ElementProps, EntityProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; +import { Code, ElementProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; @@ -15,7 +15,6 @@ import { ElementDrivesElement, ElementDrivesElementProps } from "../Relationship import { Schema, Schemas } from "../Schema"; import { HubWrappers } from "./IModelTestUtils"; import { KnownTestLocations } from "./KnownTestLocations"; -import { on } from "node:events"; chai.use(chaiAsPromised); /** 1. What is Change Propagation?** @@ -187,8 +186,8 @@ export class TopologicalSorter { for (const node of graph.nodes()) { inDegree.set(node, 0); } - for (const { from, to } of graph.edges()) { - inDegree.set(to, (inDegree.get(to) ?? 0) + 1); + for (const edge of graph.edges()) { + inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1); } if (updated) { From 3f817853811eabd20283a217dc9f968c985de1e4 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 14 Oct 2025 07:56:28 -0400 Subject: [PATCH 10/17] remove only --- common/api/core-bentley.api.md | 2 + core/backend/src/test/EDE.test.ts | 161 +++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/common/api/core-bentley.api.md b/common/api/core-bentley.api.md index 2218cb9eff9..13cfb859587 100644 --- a/common/api/core-bentley.api.md +++ b/common/api/core-bentley.api.md @@ -360,12 +360,14 @@ export enum DbResult { BE_SQLITE_ERROR_InvalidChangeSetVersion = 234881034, BE_SQLITE_ERROR_InvalidProfileVersion = 117440522, BE_SQLITE_ERROR_NoPropertyTable = 50331658, + BE_SQLITE_ERROR_NOTOPEN = 16777217, BE_SQLITE_ERROR_NoTxnActive = 83886090, BE_SQLITE_ERROR_ProfileTooNew = 201326602, BE_SQLITE_ERROR_ProfileTooNewForReadWrite = 184549386, BE_SQLITE_ERROR_ProfileTooOld = 167772170, BE_SQLITE_ERROR_ProfileTooOldForReadWrite = 150994954, BE_SQLITE_ERROR_ProfileUpgradeFailed = 134217738, + BE_SQLITE_ERROR_PropagateChangesFailed = 33554433, BE_SQLITE_ERROR_SchemaImportFailed = 335544330, BE_SQLITE_ERROR_SchemaLockFailed = 301989898, BE_SQLITE_ERROR_SchemaTooNew = 268435466, diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/EDE.test.ts index f84447e2ce2..6aede8e3735 100644 --- a/core/backend/src/test/EDE.test.ts +++ b/core/backend/src/test/EDE.test.ts @@ -1,4 +1,4 @@ -import { BeEvent, DbResult, Id64String, IModelStatus } from "@itwin/core-bentley"; +import { BeEvent, DbResult, Id64String, IModelStatus, StopWatch } from "@itwin/core-bentley"; import { Code, ElementProps, GeometricElement3dProps, GeometryStreamBuilder, GeometryStreamProps, IModel, IModelError, RelatedElement, RelationshipProps } from "@itwin/core-common"; import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry"; import * as chai from "chai"; @@ -549,7 +549,7 @@ export class Engine { } } -describe.only("EDE Tests", () => { +describe("EDE Tests", () => { const briefcases: BriefcaseDb[] = []; let iModelId: string; async function openBriefcase(): Promise { @@ -633,7 +633,7 @@ describe.only("EDE Tests", () => { "Graph has a cycle" ); }); - it("local: build system dependencies", async () => { + it("EDE/local: build system dependencies", async () => { const graph = new Graph(); /* Example: Build system dependencies @@ -662,6 +662,40 @@ describe.only("EDE Tests", () => { graph.addEdge("util.o", ["app.exe", "test.exe"]); graph.addEdge("config.json", ["app.exe"]); + // create graph + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const monitor = new ElementDrivesElementEventMonitor(b1); + await Engine.createGraph(b1, modelId, graph); + b1.saveChanges(); + b1.saveChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([ + ["main.c", "main.o"], + ["main.o", "test.exe"], + ["main.o", "app.exe"], + ["util.c", "util.o"], + ["util.o", "test.exe"], + ["util.o", "app.exe"], + ["test.c", "test.exe"], + ["config.json", "app.exe"] + ]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["main.o", "util.o", "test.exe", "app.exe"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["main.c", "util.c", "test.c", "config.json"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + + // update main.c + monitor.clear(); + await Engine.updateNode(b1, "main.c"); + b1.saveChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([ + ["main.c", "main.o"], + ["main.o", "test.exe"], + ["main.o", "app.exe"], + ]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["main.o", "test.exe", "app.exe"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["main.c"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + // Topological sort (depth-first) const df = TopologicalSorter.sortDepthFirst(graph); chai.expect(TopologicalSorter.validate(graph, df)).to.be.true; @@ -672,8 +706,18 @@ describe.only("EDE Tests", () => { chai.expect(df).to.deep.equal(["config.json", "test.c", "util.c", "util.o", "main.c", "main.o", "test.exe", "app.exe"]); chai.expect(bf).to.deep.equal(["main.c", "util.c", "test.c", "config.json", "main.o", "util.o", "app.exe", "test.exe"]); }); - it("local: complex, subset", async () => { + it("EDE/local: complex, subset", async () => { const graph = new Graph(); + /* + Adjacency: + Socks -> Shoes + Underwear -> Shoes, Pants + Pants -> Belt, Shoes + Shirt -> Belt, Tie + Tie -> Jacket + Belt -> Jacket + Watch (isolated) + */ graph.addEdge("Socks", ["Shoes"]); graph.addEdge("Underwear", ["Shoes", "Pants"]); @@ -683,6 +727,37 @@ describe.only("EDE Tests", () => { graph.addEdge("Belt", ["Jacket"]); graph.addNode("Watch"); + // Test using EDE + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const monitor = new ElementDrivesElementEventMonitor(b1); + await Engine.createGraph(b1, modelId, graph); + b1.saveChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([ + ["Socks", "Shoes"], + ["Underwear", "Shoes"], + ["Underwear", "Pants"], + ["Pants", "Shoes"], + ["Pants", "Belt"], + ["Shirt", "Belt"], + ["Belt", "Jacket"], + ["Shirt", "Tie"], + ["Tie", "Jacket"] + ]); + + // Watch is missing as it is not connected to any other node. + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["Pants", "Shoes", "Belt", "Tie", "Jacket"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["Socks", "Underwear", "Shirt"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + + monitor.clear(); + await Engine.updateNode(b1, "Socks"); + b1.saveChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([["Socks", "Shoes"]]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["Shoes"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["Socks"]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + const sorted = TopologicalSorter.sortDepthFirst(graph); chai.expect(TopologicalSorter.validate(graph, sorted)).to.be.true; chai.expect(sorted).to.deep.equal(["Watch", "Shirt", "Tie", "Underwear", "Pants", "Belt", "Jacket", "Socks", "Shoes"]); @@ -769,7 +844,7 @@ describe.only("EDE Tests", () => { chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]); chai.expect(monitor.onDeletedDependency).to.deep.equal([["B", "E"]]); }); - it.only("EDE: cyclical throw exception", async () => { + it("EDE: cyclical throw exception", async () => { const b1 = await openBriefcase(); const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); @@ -793,4 +868,80 @@ describe.only("EDE Tests", () => { chai.expect(monitor.onDeletedDependency).to.deep.equal([]); monitor.clear(); }); + it("EDE: cyclical graph can start propagation with no clear starting element", async () => { + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const graph = new Graph(); + // Graph structure with a cycle: + // A + // / \ + // B - C + + // order of insertion effect graph with cycles. + graph.addNode("B"); + graph.addNode("A"); + graph.addNode("C"); + + graph.addEdge("A", ["B"]); + graph.addEdge("B", ["C"]); + graph.addEdge("C", ["A"]); + + const monitor = new ElementDrivesElementEventMonitor(b1); + // create a network + await Engine.createGraph(b1, modelId, graph); + chai.expect(() => b1.saveChanges()).to.throw("Could not save changes due to propagation failure."); + b1.abandonChanges(); + chai.expect(monitor.onRootChanged).to.deep.equal([["C", "A"], ["A", "B"], ["B", "C"]]); + chai.expect(monitor.onAllInputsHandled).to.deep.equal(["A", "B", "C"]); + chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]); + chai.expect(monitor.onDeletedDependency).to.deep.equal([]); + monitor.clear(); + }); + it("EDE: performance", async () => { + const b1 = await openBriefcase(); + const { modelId, } = await Engine.initialize(b1); + const graph = new Graph(); + + const createTree = (depth: number, breadth: number, prefix: string) => { + if (depth === 0) + return; + for (let i = 0; i < breadth; i++) { + const node = `${prefix}${i}`; + graph.addNode(node); + if (depth > 1) { + for (let j = 0; j < breadth; j++) { + const child = `${prefix}${i}${j}`; + graph.addEdge(node, [child]); + createTree(depth - 1, breadth, `${prefix}${i}${j}`); + } + } + } + }; + + const stopWatch0 = new StopWatch("create graph", true); + createTree(5, 3, "N"); + await Engine.createGraph(b1, modelId, graph); + stopWatch0.stop(); + const createGraphTime = stopWatch0.elapsed.seconds; + + let onRootChangedCount = 0; + let onDeletedDependencyCount = 0; + let onAllInputsHandledCount = 0; + let onBeforeOutputsHandledCount = 0; + InputDrivesOutput.events.onRootChanged.addListener(() => { onRootChangedCount++; }); + InputDrivesOutput.events.onDeletedDependency.addListener(() => { onDeletedDependencyCount++; }); + NodeElement.events.onAllInputsHandled.addListener((_id: Id64String) => { onAllInputsHandledCount++; }); + NodeElement.events.onBeforeOutputsHandled.addListener(() => { onBeforeOutputsHandledCount++; }); + + const stopWatch1 = new StopWatch("save changes", true); + b1.saveChanges(); + stopWatch1.stop(); + const saveChangesTime = stopWatch1.elapsed.seconds; + chai.expect(onRootChangedCount).to.be.equals(7380); + chai.expect(onDeletedDependencyCount).to.equal(0); + chai.expect(onAllInputsHandledCount).to.be.equals(7380); + chai.expect(onBeforeOutputsHandledCount).to.equal(2460); + chai.expect(createGraphTime).to.be.lessThan(3); + chai.expect(saveChangesTime).to.be.lessThan(3); + }); }); From 49989edef777ae8b7c4161cda03fa81cc62b481d Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 14 Oct 2025 07:57:35 -0400 Subject: [PATCH 11/17] rush changes --- .../core-backend/affank-ede-test_2025-10-14-11-57.json | 10 ++++++++++ .../core-bentley/affank-ede-test_2025-10-14-11-57.json | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 common/changes/@itwin/core-backend/affank-ede-test_2025-10-14-11-57.json create mode 100644 common/changes/@itwin/core-bentley/affank-ede-test_2025-10-14-11-57.json diff --git a/common/changes/@itwin/core-backend/affank-ede-test_2025-10-14-11-57.json b/common/changes/@itwin/core-backend/affank-ede-test_2025-10-14-11-57.json new file mode 100644 index 00000000000..2e87b734906 --- /dev/null +++ b/common/changes/@itwin/core-backend/affank-ede-test_2025-10-14-11-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "Fix crash when their is a cycle in EDE.", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-bentley/affank-ede-test_2025-10-14-11-57.json b/common/changes/@itwin/core-bentley/affank-ede-test_2025-10-14-11-57.json new file mode 100644 index 00000000000..cd0bae96205 --- /dev/null +++ b/common/changes/@itwin/core-bentley/affank-ede-test_2025-10-14-11-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-bentley", + "comment": "Fix crash when their is a cycle in EDE.", + "type": "none" + } + ], + "packageName": "@itwin/core-bentley" +} \ No newline at end of file From ced66be94a3bbc99a931aef1908afd29a9eab4ca Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 14 Oct 2025 09:22:40 -0400 Subject: [PATCH 12/17] rename EDE.test.ts to ElementDrivesElement.test.ts --- .../src/test/{EDE.test.ts => ElementDrivesElement.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/backend/src/test/{EDE.test.ts => ElementDrivesElement.test.ts} (100%) diff --git a/core/backend/src/test/EDE.test.ts b/core/backend/src/test/ElementDrivesElement.test.ts similarity index 100% rename from core/backend/src/test/EDE.test.ts rename to core/backend/src/test/ElementDrivesElement.test.ts From 643c84e358d9efbb26f17e8dd5df0a84b1005292 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 14 Oct 2025 10:47:12 -0400 Subject: [PATCH 13/17] skip perf test --- core/backend/src/test/ElementDrivesElement.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/ElementDrivesElement.test.ts b/core/backend/src/test/ElementDrivesElement.test.ts index 6aede8e3735..41c98b8ac0d 100644 --- a/core/backend/src/test/ElementDrivesElement.test.ts +++ b/core/backend/src/test/ElementDrivesElement.test.ts @@ -897,7 +897,7 @@ describe("EDE Tests", () => { chai.expect(monitor.onDeletedDependency).to.deep.equal([]); monitor.clear(); }); - it("EDE: performance", async () => { + it.skip("EDE: performance", async () => { const b1 = await openBriefcase(); const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); From 9e9afcc386601eae353a59bacabe661d29136c34 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 14 Oct 2025 14:25:02 -0400 Subject: [PATCH 14/17] skip flaky test --- core/backend/src/test/ecdb/ConcurrentQuery.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/backend/src/test/ecdb/ConcurrentQuery.test.ts b/core/backend/src/test/ecdb/ConcurrentQuery.test.ts index d417291026c..46f2f04acae 100644 --- a/core/backend/src/test/ecdb/ConcurrentQuery.test.ts +++ b/core/backend/src/test/ecdb/ConcurrentQuery.test.ts @@ -110,7 +110,7 @@ describe("ConcurrentQuery", () => { db.close(); }); - it("restart query", async () => { + it.skip("restart query #flaky", async () => { const testFile = IModelTestUtils.resolveAssetFile("test.bim"); const db = SnapshotDb.openFile(testFile); ConcurrentQuery.resetConfig(db[_nativeDb], { globalQuota: { time: 60, memory: 10000000 }, progressOpCount: 1000 }); @@ -132,7 +132,7 @@ describe("ConcurrentQuery", () => { await delay(1); const resp2 = ConcurrentQuery.executeQueryRequest(db[_nativeDb], req1); const resp = await Promise.all([resp1, resp2]); - expect(resp[0].status).equals(DbResponseStatus.Cancel); + expect(resp[0].status).equals(DbResponseStatus.Cancel); // can result in DbResponseStatus.Partial instead of DbResponseStatus.Cancel expect(resp[1].status).equals(DbResponseStatus.Done); db.close(); }); From 9374fc251a0294d21a3b837a3814765554c6daf6 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Wed, 15 Oct 2025 16:30:05 -0400 Subject: [PATCH 15/17] fixes --- .../src/test/ElementDrivesElement.test.ts | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/core/backend/src/test/ElementDrivesElement.test.ts b/core/backend/src/test/ElementDrivesElement.test.ts index 41c98b8ac0d..7445e0697cc 100644 --- a/core/backend/src/test/ElementDrivesElement.test.ts +++ b/core/backend/src/test/ElementDrivesElement.test.ts @@ -549,7 +549,7 @@ export class Engine { } } -describe("EDE Tests", () => { +describe.only("ElementDrivesElement Tests", () => { const briefcases: BriefcaseDb[] = []; let iModelId: string; async function openBriefcase(): Promise { @@ -575,16 +575,6 @@ describe("EDE Tests", () => { }); it("local: topological sort", async () => { const graph = new Graph(); - // Graph structure: - // 1 - // / \ - // 2 3 - // |\ \ - // | \ \ - // 4 5 4 - // \ / - // \ / - // 5 graph.addEdge("1", ["2", "3"]); graph.addEdge("2", ["5", "4"]); @@ -612,16 +602,10 @@ describe("EDE Tests", () => { const df = TopologicalSorter.sortDepthFirst(graph, [], false); chai.expect(TopologicalSorter.validate(graph, df)).to.be.false; chai.expect(df).to.deep.equal(["1", "2", "3"]); - - // const bf = TopologicalSorter.sortBreadthFirst(graph, [], false); - // chai.expect(TopologicalSorter.validate(graph, bf)).to.be.false; - // chai.expect(bf).to.deep.equal(["1", "2", "3"]); }); it("local: cycle detection (throw)", async () => { const graph = new Graph(); // Graph structure: - // A---B---C - //. \ D / // 1 --> 2 --> 3 // ^ | // |-----------| @@ -660,7 +644,8 @@ describe("EDE Tests", () => { graph.addEdge("test.c", ["test.exe"]); graph.addEdge("main.o", ["app.exe", "test.exe"]); graph.addEdge("util.o", ["app.exe", "test.exe"]); - graph.addEdge("config.json", ["app.exe"]); + graph.addEdge("config.json", ["app.exe", "test.exe"]); + // create graph const b1 = await openBriefcase(); @@ -677,7 +662,8 @@ describe("EDE Tests", () => { ["util.o", "test.exe"], ["util.o", "app.exe"], ["test.c", "test.exe"], - ["config.json", "app.exe"] + ["config.json", "test.exe"], + ["config.json", "app.exe"], ]); chai.expect(monitor.onAllInputsHandled).to.deep.equal(["main.o", "util.o", "test.exe", "app.exe"]); chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["main.c", "util.c", "test.c", "config.json"]); @@ -709,14 +695,14 @@ describe("EDE Tests", () => { it("EDE/local: complex, subset", async () => { const graph = new Graph(); /* - Adjacency: - Socks -> Shoes - Underwear -> Shoes, Pants - Pants -> Belt, Shoes - Shirt -> Belt, Tie - Tie -> Jacket - Belt -> Jacket - Watch (isolated) + Graph shows what must be put on before other items: + - Socks before Shoes + - Underwear before Shoes and Pants + - Pants before Belt and Shoes + - Shirt before Belt and Tie + - Tie before Jacket + - Belt before Jacket + - Watch has no dependencies */ graph.addEdge("Socks", ["Shoes"]); @@ -794,15 +780,16 @@ describe("EDE Tests", () => { const graph = new Graph(); // Graph structure: - // A - // / \ - // B C - // |\ \ - // | \ \ - // E D D - // \ / - // \ / - // E + // A + // / \ + // B C + // /| | + // E | | + // | | + // D <-+ + // | + // v + // E graph.addEdge("A", ["B", "C"]); graph.addEdge("B", ["E", "D"]); graph.addEdge("C", ["D"]); From 98e8335d55a03b5f5540f206fb263584e65e421c Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Wed, 15 Oct 2025 16:30:39 -0400 Subject: [PATCH 16/17] update graphs --- core/backend/src/test/ElementDrivesElement.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/ElementDrivesElement.test.ts b/core/backend/src/test/ElementDrivesElement.test.ts index 7445e0697cc..fe37f1a87cb 100644 --- a/core/backend/src/test/ElementDrivesElement.test.ts +++ b/core/backend/src/test/ElementDrivesElement.test.ts @@ -549,7 +549,7 @@ export class Engine { } } -describe.only("ElementDrivesElement Tests", () => { +describe("ElementDrivesElement Tests", () => { const briefcases: BriefcaseDb[] = []; let iModelId: string; async function openBriefcase(): Promise { From 1e20343a989e6043fab0fee18b15a6d2060f08ea Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 16 Oct 2025 09:56:06 -0400 Subject: [PATCH 17/17] fix --- .../src/test/ElementDrivesElement.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/core/backend/src/test/ElementDrivesElement.test.ts b/core/backend/src/test/ElementDrivesElement.test.ts index fe37f1a87cb..2fe92543ff1 100644 --- a/core/backend/src/test/ElementDrivesElement.test.ts +++ b/core/backend/src/test/ElementDrivesElement.test.ts @@ -780,16 +780,12 @@ describe("ElementDrivesElement Tests", () => { const graph = new Graph(); // Graph structure: - // A - // / \ - // B C - // /| | - // E | | - // | | - // D <-+ - // | - // v - // E + // A + // / \ + // B C + // |\ / + // | \/ + // E--D graph.addEdge("A", ["B", "C"]); graph.addEdge("B", ["E", "D"]); graph.addEdge("C", ["D"]);