UNPKG

@itwin/core-backend

Version:
862 lines (853 loc) • 37.4 kB
import { BeEvent, DbResult, IModelStatus, StopWatch } from "@itwin/core-bentley"; import { Code, GeometryStreamBuilder, IModel, IModelError, RelatedElement } 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 { IModelDb } from "../IModelDb"; import { HubMock } from "../internal/HubMock"; import { PhysicalModel } from "../Model"; import { SubjectOwnsPartitionElements } from "../NavigationRelationship"; import { ElementDrivesElement } from "../Relationship"; import { Schema, Schemas } from "../Schema"; import { HubWrappers } from "./IModelTestUtils"; import { KnownTestLocations } from "./KnownTestLocations"; 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. */ var Color; (function (Color) { Color[Color["White"] = 0] = "White"; Color[Color["Gray"] = 1] = "Gray"; Color[Color["Black"] = 2] = "Black"; })(Color || (Color = {})); export class Graph { _nodes = []; _edges = new Map(); constructor() { } addNode(node) { if (!this._nodes.includes(node)) this._nodes.push(node); } *nodes() { yield* this._nodes; } *edges() { for (const [from, toList] of this._edges.entries()) { for (const to of toList) { yield { from, to }; } } } addEdge(from, to) { 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); } } getEdges(node) { if (!this._edges.has(node)) return []; return this._edges.get(node); } clone() { 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; } toGraphvis(accessor) { // 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 class TopologicalSorter { static visit(graph, node, colors, sorted, failOnCycles) { 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); } } static sortDepthFirst(graph, updated, failOnCycles = true) { const sorted = []; 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(); } static sortBreadthFirst(graph, updated, failOnCycles = true) { const sorted = []; const queue = []; // Vector to store indegree of each vertex const inDegree = new Map(); for (const node of graph.nodes()) { inDegree.set(node, 0); } for (const edge of graph.edges()) { inDegree.set(edge.to, (inDegree.get(edge.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; } static validate(graph, sorted) { 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; } } class ElementDrivesElementEventMonitor { iModelDb; onRootChanged = []; onAllInputsHandled = []; onBeforeOutputsHandled = []; onDeletedDependency = []; constructor(iModelDb) { this.iModelDb = iModelDb; InputDrivesOutput.events.onDeletedDependency.addListener((props) => this.onDeletedDependency.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel])); InputDrivesOutput.events.onRootChanged.addListener((props) => this.onRootChanged.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel])); NodeElement.events.onAllInputsHandled.addListener((id) => this.onAllInputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel)); NodeElement.events.onBeforeOutputsHandled.addListener((id) => this.onBeforeOutputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel)); } clear() { this.onRootChanged.length = 0; this.onAllInputsHandled.length = 0; this.onBeforeOutputsHandled.length = 0; this.onDeletedDependency.length = 0; } } export class InputDrivesOutput extends ElementDrivesElement { static events = { onRootChanged: new BeEvent(), onDeletedDependency: new BeEvent(), }; static get className() { return "InputDrivesOutput"; } constructor(props, iModel) { super(props, iModel); } static onRootChanged(props, iModel) { this.events.onRootChanged.raiseEvent(props, iModel); } static onDeletedDependency(props, iModel) { this.events.onDeletedDependency.raiseEvent(props, iModel); } } export class NodeElement extends GeometricElement3d { op; val; static events = { onAllInputsHandled: new BeEvent(), onBeforeOutputsHandled: new BeEvent(), }; static get className() { return "Node"; } constructor(props, iModel) { super(props, iModel); this.op = props.op; this.val = props.val; } toJSON() { const val = super.toJSON(); val.op = this.op; val.val = this.val; return val; } static onAllInputsHandled(id, iModel) { this.events.onAllInputsHandled.raiseEvent(id, iModel); } static onBeforeOutputsHandled(id, iModel) { this.events.onBeforeOutputsHandled.raiseEvent(id, iModel); } static generateGeometry(radius) { 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; } static getCategory(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 { static get schemaName() { return "Network"; } static registerSchema() { if (this !== Schemas.getRegisteredSchema(NetworkSchema.schemaName)) { Schemas.registerSchema(this); ClassRegistry.register(NodeElement, this); ClassRegistry.register(InputDrivesOutput, this); } } static async importSchema(iModel) { if (iModel.querySchemaVersion("Network")) return; const schema1 = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="Network" alias="net" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="Node"> <BaseClass>bis:GraphicalElement3d</BaseClass> <ECProperty propertyName="op" typeName="string" /> <ECProperty propertyName="val" typeName="double" /> </ECEntityClass> <ECRelationshipClass typeName="InputDrivesOutput" modifier="None" strength="referencing"> <BaseClass>bis:ElementDrivesElement</BaseClass> <Source multiplicity="(0..1)" roleLabel="drives" polymorphic="true"> <Class class="Node"/> </Source> <Target multiplicity="(0..*)" roleLabel="is driven by" polymorphic="false"> <Class class="Node"/> </Target> <ECProperty propertyName="prop" typeName="double" /> </ECRelationshipClass> </ECSchema>`; await iModel.importSchemaStrings([schema1]); } } export class Engine { static async createGraph(iModelDb, modelId, graph) { const nodes = new Map(); const outGraph = new Graph(); 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, 0); outGraph.addEdge(nodes.get(edge.from), nodes.get(edge.to)); } return outGraph; } static countNodes(iModelDb) { // 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(); } return 0; }); } static countEdges(iModelDb) { // 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(); } return 0; }); } static queryEdgesForSource(iModelDb, sourceId) { const edges = []; // 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) { edges.push({ id: stmt.getValue(0).getId(), classFullName: InputDrivesOutput.classFullName, sourceId: stmt.getValue(1).getId(), targetId: stmt.getValue(2).getId(), prop: stmt.getValue(3).getDouble(), status: stmt.getValue(4).getInteger(), priority: stmt.getValue(5).getInteger(), }); } }); return edges; } static queryEdgesForTarget(iModelDb, targetId) { const edges = []; // 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) { edges.push({ id: stmt.getValue(0).getId(), classFullName: InputDrivesOutput.classFullName, sourceId: stmt.getValue(1).getId(), targetId: stmt.getValue(2).getId(), prop: stmt.getValue(3).getDouble(), status: stmt.getValue(4).getInteger(), priority: stmt.getValue(5).getInteger(), }); } }); return edges; } static async createPartition(iModelDb) { const parentId = new SubjectOwnsPartitionElements(IModel.rootSubjectId); const modelId = IModel.repositoryModelId; const modeledElementProps = { 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()); } static async createModel(iModelDb) { 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; } static async createNodeCategory(iModelDb) { const category = SpatialCategory.create(iModelDb, IModelDb.dictionaryId, NodeElement.classFullName); return category.insert(); } static async initialize(iModelDb) { await NetworkSchema.importSchema(iModelDb); NetworkSchema.registerSchema(); const modelId = await this.createModel(iModelDb); const categoryId = await this.createNodeCategory(iModelDb); return { modelId, categoryId, }; } static async insertNode(iModelDb, modelId, name, op, val, location, radius = 0.1) { const props = { 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); } static async deleteNode(iModelDb, nodeId) { await iModelDb.locks.acquireLocks({ exclusive: nodeId }); return iModelDb.elements.deleteElement(nodeId); } static async updateNodeProps(iModelDb, props) { await iModelDb.locks.acquireLocks({ exclusive: props.id }); return iModelDb.elements.updateElement(props); } static async updateNode(iModelDb, userLabel) { // 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) return stmt.getValue(0).getId(); return undefined; }); if (!id) { throw new Error(`Node with userLabel ${userLabel} not found`); } await this.updateNodeProps(iModelDb, { id }); } static async deleteEdge(iModelDb, from, to) { // 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(), }; return undefined; }); if (!edge) { throw new Error(`Edge from ${from} to ${to} not found`); } iModelDb.relationships.deleteInstance(edge); } static async insertEdge(iModelDb, sourceId, targetId, prop) { const props = { classFullName: InputDrivesOutput.classFullName, sourceId, targetId, prop, status: 0, priority: 0 }; return iModelDb.relationships.insertInstance(props); } } describe("ElementDrivesElement Tests", () => { const briefcases = []; let iModelId; async function openBriefcase() { 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 () => { 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.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"]); 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"]); }); 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"]); }); 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("EDE/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", "test.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", "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; // 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("EDE/local: complex, subset", async () => { const graph = new Graph(); /* 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"]); 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"); // 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"]); // 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"]); const sorted3 = TopologicalSorter.sortDepthFirst(graph, ["Shoes"]); chai.expect(sorted3).to.deep.equal(["Shoes"]); const sorted4 = TopologicalSorter.sortDepthFirst(graph, ["Socks"]); chai.expect(sorted4).to.deep.equal(["Socks", "Shoes"]); const sorted5 = TopologicalSorter.sortDepthFirst(graph, ["Tie"]); chai.expect(sorted5).to.deep.equal(["Tie", "Jacket"]); 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("EDE: basic graph operations", async () => { const b1 = await openBriefcase(); const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); // Graph structure: // A // / \ // B C // |\ / // | \/ // E--D graph.addEdge("A", ["B", "C"]); graph.addEdge("B", ["E", "D"]); graph.addEdge("C", ["D"]); graph.addEdge("D", ["E"]); 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"], ["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(); // update a node in network await Engine.updateNode(b1, "B"); b1.saveChanges(); 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([]); 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"]]); }); it("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(); }); 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.skip("EDE: performance", async () => { const b1 = await openBriefcase(); const { modelId, } = await Engine.initialize(b1); const graph = new Graph(); const createTree = (depth, breadth, prefix) => { 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) => { 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); }); }); //# sourceMappingURL=ElementDrivesElement.test.js.map