瀏覽代碼

WIP: A trivial parser + wrote a test for delta_executor.

Joeri Exelmans 2 年之前
父節點
當前提交
7f1000e868
共有 5 個文件被更改,包括 246 次插入28 次删除
  1. 53 0
      src/onion/delta_executor.test.ts
  2. 32 1
      src/onion/delta_executor.ts
  3. 117 0
      src/onion/graph_state.ts
  4. 25 27
      src/onion/parser.test.ts
  5. 19 0
      src/util/partial_ordering.ts

+ 53 - 0
src/onion/delta_executor.test.ts

@@ -0,0 +1,53 @@
+import {GraphDeltaExecutor, GraphStateManipulator} from "./delta_executor";
+import {GraphState} from "./graph_state";
+import {mockUuid} from "./test_helpers";
+
+import {
+  NodeCreation,
+  NodeDeletion,
+  EdgeCreation,
+  EdgeUpdate,
+} from "./primitive_delta";
+
+import {
+  assert,
+} from "../util/assert";
+
+describe("GraphDeltaExecutor", () => {
+  // need to test more scenarios!
+
+  // taken from primitive delta test:
+  it("Delete node with self-edge", () => {
+    const graphState = new GraphState();
+    const executor = new GraphDeltaExecutor(graphState);
+    const getId = mockUuid();
+
+    const nodeCreation = new NodeCreation(getId());
+    const edgeCreation = new EdgeCreation(nodeCreation, "label", nodeCreation);
+    const edgeUpdate = new EdgeUpdate(edgeCreation, null);
+    const nodeDeletion = new NodeDeletion(nodeCreation, [edgeUpdate], [edgeUpdate]);
+
+    assert(graphState.nodes.size === 0, "Expected no nodes initially");
+    assert(graphState.edges.length === 0, "Expected no edges initially");
+
+    executor.exec(nodeCreation);
+
+    assert(graphState.nodes.size === 1, "Expected one node after NodeCreation");
+    assert(graphState.edges.length === 0, "Expected no edges after NodeCreation");
+
+    executor.exec(edgeCreation);
+
+    assert(graphState.nodes.size === 1, "Expected one node after EdgeCreation");
+    assert(graphState.edges.length === 1, "Expected one edge after EdgeCreation");
+
+    executor.exec(edgeUpdate);
+
+    assert(graphState.nodes.size === 1, "Expected one node after EdgeUpdate");
+    assert(graphState.edges.length === 0, "Expected no edges after EdgeUpdate");
+
+    executor.exec(nodeDeletion);
+
+    assert(graphState.nodes.size === 0, "Expected no nodes after NodeDeletion");
+    assert(graphState.edges.length === 0, "Expected no edges after NodeDeletion");
+  });
+});

+ 32 - 1
src/onion/delta_executor.ts

@@ -25,6 +25,38 @@ export interface GraphStateManipulator {
   deleteLink(sourceId: PrimitiveValue, label: string);
 }
 
+// A 'proxy' GraphStateManipulator that multicasts graph state operations to a bunch of GraphStateManipulators.
+export class FanOutManipulator implements GraphStateManipulator {
+  readonly manipulators: GraphStateManipulator[];
+
+  constructor(manipulators: GraphStateManipulator[]) {
+    this.manipulators = manipulators;
+  }
+  
+  createNode(ns: NodeState) {
+    this.manipulators.forEach(m => m.createNode(ns));
+  }
+  createValue(vs: ValueState) {
+    this.manipulators.forEach(m => m.createValue(vs));
+  }
+  deleteNode(id: PrimitiveValue) {
+    this.manipulators.forEach(m => m.deleteNode(id));
+  }
+  deleteValue(value: PrimitiveValue) {
+    this.manipulators.forEach(m => m.deleteValue(value));
+  }
+  createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {
+    this.manipulators.forEach(m => m.createLinkToNode(sourceId, label, targetId));
+  }
+  createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {
+    this.manipulators.forEach(m => m.createLinkToValue(sourceId, label, targetValue));
+  }
+  deleteLink(sourceId: PrimitiveValue, label: string) {
+    this.manipulators.forEach(m => m.deleteLink(sourceId, label));
+  }
+}
+
+
 type IncomingEdgeDelta = EdgeCreation|EdgeUpdate|NodeDeletion;
 
 // For one (ordinary or value) node of a graph state, captures the Deltas that created, updated or deleted an incoming edge.
@@ -206,7 +238,6 @@ export class ValueState extends NodeOrValueState {
   }
 }
 
-
 // Executes (primitive) deltas, and updates the graph state accordingly (through GraphStateManipulator)
 // Decouples execution of deltas from any specific graph state representation (e.g. d3).
 export class GraphDeltaExecutor {

+ 117 - 0
src/onion/graph_state.ts

@@ -0,0 +1,117 @@
+import {GraphStateManipulator, NodeState, ValueState} from "./delta_executor";
+import {PrimitiveValue} from "./types";
+
+class Node {
+  uuid: PrimitiveValue;
+  incoming: Edge[] = [];
+  outgoing: Map<string, Edge> = new Map();
+
+  constructor(uuid: PrimitiveValue) {
+    this.uuid = uuid;
+  }
+}
+
+class Value {
+  value: PrimitiveValue;
+  incoming: Edge[] = [];
+
+  constructor(value: PrimitiveValue) {
+    this.value = value;
+  }
+}
+
+class Edge {
+  source: Node;
+  label: string;
+  target: Node | Value;
+
+  constructor(source: Node, label: string, target: Node | Value) {
+    this.source = source;
+    this.label = label;
+    this.target = target;
+  }
+}
+
+// An implementation of GraphStateManipulator that builds an in-memory graph.
+// Useful for tests.
+// Most operations are O(log(n)), some O(n).
+// The fronted uses D3StateManipulator instead.
+export class GraphState implements GraphStateManipulator {
+  nodes: Map<PrimitiveValue, Node> = new Map();
+  values: Map<PrimitiveValue, Value> = new Map();
+  edges: Edge[] = [];
+
+  createNode(ns: NodeState) {
+    const uuid = ns.creation.id.value;
+    this.nodes.set(uuid, new Node(uuid));
+  }
+  createValue(vs: ValueState) {
+    const value = vs.value;
+    this.values.set(value, new Value(value));
+  }
+  private getNode(id: PrimitiveValue): Node {
+    const node = this.nodes.get(id);
+    if (node === undefined) {
+      throw new Error("Non-existing node: " + JSON.stringify(id));
+    }
+    return node;
+  }
+  private getValue(value: PrimitiveValue): Value {
+    const v = this.values.get(value);
+    if (v === undefined) {
+      throw new Error("Non-existing value: " + JSON.stringify(value));
+    }
+    return v;
+  }
+  private assertNoOutgoing(sourceNode: Node, label: string) {
+    if (sourceNode.outgoing.has(label)) {
+      throw new Error("Source (already) has outgoing edge with label " + label);
+    }
+  }
+  deleteNode(id: PrimitiveValue) {
+    const node = this.getNode(id);
+    if (node.incoming.length !== 0) {
+      throw new Error("Cannot delete node that has non-zero number of incoming edges");
+    }
+    if (node.outgoing.size !== 0) {
+      throw new Error("Cannot delete node that has non-zero number of outgoing edges");
+    }
+    this.nodes.delete(id);
+  }
+  deleteValue(v: PrimitiveValue) {
+    const value = this.getValue(v);
+    if (value.incoming.length !== 0) {
+      throw new Error("Cannot delete value that has non-zero number of incoming edges");
+    }
+    this.values.delete(v);
+  }
+  createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {
+    const sourceNode = this.getNode(sourceId);
+    const targetNode = this.getNode(targetId);
+    this.assertNoOutgoing(sourceNode, label);
+    const edge = new Edge(sourceNode, label, targetNode);
+    sourceNode.outgoing.set(label, edge);
+    targetNode.incoming.push(edge);
+    this.edges.push(edge);
+  }
+  createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {
+    const sourceNode = this.getNode(sourceId);
+    const value = this.getValue(targetValue);
+    this.assertNoOutgoing(sourceNode, label);
+    const edge = new Edge(sourceNode, label, value);
+    sourceNode.outgoing.set(label, edge);
+    value.incoming.push(edge);
+    this.edges.push(edge);
+  }
+  deleteLink(sourceId: PrimitiveValue, label: string) {
+    const sourceNode = this.getNode(sourceId);
+    const edge = sourceNode.outgoing.get(label);
+    if (edge === undefined) {
+      throw new Error("Cannot delete non-existing link with source " + JSON.stringify(sourceId) + " and label " + label);
+    }
+    sourceNode.outgoing.delete(label);
+    const incoming = edge.target.incoming;
+    incoming.splice(incoming.findIndex(e => e === edge), 1);
+    this.edges.splice(this.edges.findIndex(e => e === edge), 1);
+  }
+}

+ 25 - 27
src/onion/parser.test.ts

@@ -17,6 +17,8 @@ import {
 } from "./composite_delta";
 import {mockUuid} from "./test_helpers";
 import {UUID} from "./types";
+import {assert} from "../util/assert";
+import {visitPartialOrdering} from "../util/partial_ordering";
 
 function getParentLink(cs: Version, parentCorr: Version): [Version,Delta] {
   const parentCsEmbedding = parentCorr.getEmbedded("cs");
@@ -70,7 +72,7 @@ class TrivialParser implements Parser {
     }
 
     // these are the new deltas in 'cs'
-    const primitives = [...lvl1CsDelta.iterPrimitiveDeltas()];
+    const csDeltas = [...lvl1CsDelta.iterPrimitiveDeltas()];
     const parentCorrDeltas = new Set(parentCorr.iterPrimitiveDeltas());
 
     const asDeltas: Delta[] = [];
@@ -79,21 +81,21 @@ class TrivialParser implements Parser {
     const csOverrides = new Map();
     const asOverrides = new Map();
 
-    for (const p of primitives) {
-      if (p instanceof NodeCreation) {
+    for (const csDelta of csDeltas) {
+      if (csDelta instanceof NodeCreation) {
         const corrCreation = new NodeCreation(this.getUuid());
-        const corr2Cs = new EdgeCreation(corrCreation, "cs", p);
+        const corr2Cs = new EdgeCreation(corrCreation, "cs", csDelta);
         const asCreation = new NodeCreation(this.getUuid());
         const corr2As = new EdgeCreation(corrCreation, "as", asCreation);
 
         asDeltas.push(asCreation);
         corrDeltas.push(corrCreation, corr2Cs, corr2As);
       }
-      else if (p instanceof NodeDeletion) {
-        const csCreation = p.creation; // the NodeCreation of the deleted cs node
+      else if (csDelta instanceof NodeDeletion) {
+        const csCreation = csDelta.creation; // the NodeCreation of the deleted cs node
 
-        // p will conflict with our earlier 'corr2Cs' EdgeCreation delta:
-        const corr2Cs = p.edgeTargetConflicts.find(e => parentCorrDeltas.has(e));
+        // csDelta will conflict with our earlier 'corr2Cs' EdgeCreation delta:
+        const corr2Cs = csDelta.edgeTargetConflicts.find(e => parentCorrDeltas.has(e));
         if (corr2Cs === undefined) {
           throw new Error("Assertion failed: When a node is deleted, the deletion must be conflicting with the creation of an incoming correspondence edge.");
         }
@@ -118,7 +120,7 @@ class TrivialParser implements Parser {
         // We already have the deletion in the CS model, so we only need to create another one to be used in the CORR model:
         const csDeletion1 = new NodeDeletion(csCreation, [], [corrDeletion]);
 
-        csOverrides.set(p, csDeletion1);
+        csOverrides.set(csDelta, csDeletion1);
         asOverrides.set(asDeletion, asDeletion1);
 
         asDeltas.push(asDeletion);
@@ -126,30 +128,26 @@ class TrivialParser implements Parser {
       }
     }
 
-    // L1-deltas for AS and CORR:
-    const asComposite = this.asLvl.createComposite(asDeltas);
-
-    const csDeltas1 = lvl1CsDelta.deltas.map(d => {
-      const result = csOverrides.get(d) || d;
-      // console.log("overridden or not:", result);
-      return result;
-    });
-
-    const asDeltas1 = asDeltas.map(d => asOverrides.get(d) || d);
-
     // New AS-version:
+    const asComposite = this.asLvl.createComposite(asDeltas);
     const as = this.registry.createVersion(parentAs.embedded, asComposite);
 
-    // L2-delta, where changes to CS, AS and CORR are a single transaction:
-    const corrDelta = this.corrLvl.createComposite([...csDeltas1, ...asDeltas1, ...corrDeltas]);
-
     // New CORR-version:
-    return this.registry.createVersion(parentCorr, corrDelta, embed(["cs", cs, csOverrides], ["as", as, asOverrides]));
+    const csDeltas1 = csDeltas.map(d => csOverrides.get(d) || d);
+    const asDeltas1 = asDeltas.map(d => asOverrides.get(d) || d);
+    // the order in which corr-deltas are put in corrComposite matters - deltas must occur after their dependencies:
+    const orderedByDependency: Delta[] = [];
+    visitPartialOrdering(
+      [...csDeltas1, ...asDeltas1, ...corrDeltas],
+      (d: Delta) => d.getDependencies(),
+      (d: Delta) => orderedByDependency.push(d));
+    const corrComposite = this.corrLvl.createComposite(orderedByDependency);
+    return this.registry.createVersion(parentCorr, corrComposite, embed(["cs", cs, csOverrides], ["as", as, asOverrides]));
   }
 }
 
 describe("Parser", () => {
-  it("Parse CS creation", () => {
+  it("Parse CS creation and deletion", () => {
     const registry = new VersionRegistry();
     const csLvl = new CompositeLevel();
     const asLvl = new CompositeLevel();
@@ -167,9 +165,7 @@ describe("Parser", () => {
     const csV1 = registry.createVersion(csInitial, csLvl.createComposite([csCreation]));
     const csV2 = registry.createVersion(csV1, csLvl.createComposite([csDeletion]));
 
-    console.log("parsing csV1...");
     const corrV1 = parser.parse(csV1, corrInitial);
-    console.log("parsing csV2...");
     const corrV2 = parser.parse(csV2, corrV1);
 
     const asV2 = corrV2.getEmbedded("as")?.embedded;
@@ -177,5 +173,7 @@ describe("Parser", () => {
       throw new Error("Expected asV2 to exist!");
     }
     
+    const asV2Primitives = [...asV2.iterPrimitiveDeltas()];
+    assert(asV2Primitives.length === 2, "Expected 2 primitive deltas on AS");
   });
 });

+ 19 - 0
src/util/partial_ordering.ts

@@ -0,0 +1,19 @@
+
+export function visitPartialOrdering<T>(elements: T[], smallerThan: (T) => T[], visitCallback: (T) => void) {
+  const visitable = new Set(elements);
+  const remaining = new Set(elements);
+  while (remaining.size > 0) {
+    let found = false;
+    for (const elem of remaining) {
+      if (smallerThan(elem).every(e => !visitable.has(e) || !remaining.has(e))) {
+        visitCallback(elem);
+        remaining.delete(elem);
+        found = true;
+        break;
+      }
+    }
+    if (!found) {
+      throw new Error("Could not find a smallest element - not a partial ordering?");
+    }
+  }
+}