소스 검색

Enable strict null checks (nullability of return values explicit). Extend test.

Joeri Exelmans 3 년 전
부모
커밋
3a4598c95e
6개의 변경된 파일1018개의 추가작업 그리고 83개의 파일을 삭제
  1. 2 1
      .gitignore
  2. 47 20
      onion.test.ts
  3. 82 56
      onion.ts
  4. 4 2
      package.json
  5. 881 3
      pnpm-lock.yaml
  6. 2 1
      tsconfig.json

+ 2 - 1
.gitignore

@@ -1 +1,2 @@
-node_modules/
+node_modules/
+.nyc_output/

+ 47 - 20
onion.test.ts

@@ -1,35 +1,62 @@
 import { UUID, GraphState } from "./onion"
 
-let nextId = 0;
-function uuidCallback() {
-  return new UUID(nextId++);
+function getUuidCallback() {
+  let nextId = 0;
+  return function() {
+    return new UUID(nextId++);
+  }
 }
 
-describe("Create and delete", () => {
-  it("Create and delete", () => {
-    const s = new GraphState(uuidCallback);
+function assert(expression, msg) {
+  if (!expression) {
+    throw new Error(msg);
+  }
+}
+
+describe("CRUD operations", () => {
+  it("Deleting a node", () => {
+    const s = new GraphState(getUuidCallback());
+
+    const n1 = s.createNode();
+    const n2 = s.createNode();
 
-    // console.log(s.nodes)
+    s.setEdge(n1, 'edge', n2);
+    s.setEdge(n2, 'x', 42);
+    s.setEdge(n2, 'y', 420);
+    s.setEdge(42, 'z', n1);
+
+    assert(s.getEdges().length === 4, "Expected 4 edges.");
+
+    s.deleteNode(n2);
+    assert(s.getEdges().length === 1, "Expected incoming and outgoing edges of n2 to be deleted.")
+  });
+
+  it("Updating an edge", () => {
+    const s = new GraphState(getUuidCallback());
 
     const n1 = s.createNode();
-    // console.log(s.nodes)
     const n2 = s.createNode();
-    // console.log(s.nodes)
 
-    s.createEdge(n1, n2, 'e');
-    // console.log(s.nodes)
-    s.createEdge(n2, 42, 'x');
-    s.createEdge(42, n1, 'y');
+    s.setEdge(n1, 'edge', n2);
+    s.setEdge(n2, 'x', 42);
+    s.setEdge(n2, 'y', 420);
+
+    assert(s.readEdge(n2, 'x') === 42, "expected n2.x === 42");
+
+    s.setEdge(n2, 'x', 43);
 
-    // console.log(s.nodes)
-    // console.log(s.getEdges())
+    assert(s.readEdge(n2, 'x') === 43, "expected n2.x === 43");
 
-    s.deleteNode(n1);
+    assert(s.readEdge(n2, 'a') === undefined, "edge should not exist.")
+  });
 
-    // console.log(s.nodes)
-    console.log(s.getEdges())
+  it("Delete non-existing node", () => {
+    const s = new GraphState(getUuidCallback());
+    s.deleteNode(new UUID('non-existent-uuid'));
+  });
 
-    s.deleteEdge(n2, 'x');
-    console.log(s.getEdges())
+  it("Read non-existing edge", () => {
+    const s = new GraphState(getUuidCallback());
+    assert(s.readEdge(new UUID('non-existent-uuid'), 'x') === undefined, "edge should not exist.");
   })
 })

+ 82 - 56
onion.ts

@@ -1,4 +1,4 @@
-// NodeJS library
+// NodeJS libraries
 import {inspect} from "util";
 
 type PrimitiveType = string | number | boolean;
@@ -30,7 +30,7 @@ function nodeIdsEqual(a: NodeId, b: NodeId) {
 // Every edge corresponds to one entry in the source's 'outgoing', and one entry in the target's 'incoming'.
 class Node {
   outgoing: Map<string, NodeId>;
-  incoming: Array<{label: string, fromId: NodeId}>;
+  incoming: Array<{label: string, srcId: NodeId}>;
 
   constructor() {
     this.outgoing = new Map();
@@ -51,27 +51,32 @@ class NodeMap {
     this.values = new Map();
   }
 
-  get(id: NodeId): Node {
+  getOptional(id: NodeId): Node | undefined {
     if (id instanceof UUID) {
       // ordinary node: can only get it if it actually exists,
       // i.e., it has already been created and has not yet been deleted.
-      if (! this.nodes.has(id.uuid)) {
-        // TODO: turn this into 'Maybe'
-        throw Error("No such node:" + id);
-      }
-      return this.nodes.get(id.uuid);
+      return this.nodes.get(id.uuid); // may return undefined
     }
 
     // value node: implicitly create it if it doesn't exist yet,
     // pretending that it's "always already there"
-    if (this.values.has(id)) {
-      return this.values.get(id);
-    } else {
-      // auto-construct non-existing value node
-      const node = new Node();
-      this.values.set(id, node);
-      return node;
+    const v = this.values.get(id);
+    if (v !== undefined) {
+      return v;
     }
+    // auto-construct non-existing value node
+    const node = new Node();
+    this.values.set(id, node);
+    return node;
+  }
+
+  // same as get, but raises error when not found
+  get(id: NodeId): Node {
+    const node = this.getOptional(id);
+    if (node === undefined) {
+      throw Error("node not found");
+    }
+    return node;
   }
 
   // create a new ordinary node
@@ -82,6 +87,7 @@ class NodeMap {
   }
 
   // delete an ordinary node
+  // Idempotent.
   delete(id: UUID) {
     this.nodes.delete(id.uuid);
   }
@@ -104,67 +110,87 @@ export class GraphState {
   }
 
   // Delete node and delete all of its outgoing + incoming edges.
+  // Does nothing when given uuid does not exist.
+  // Idempotent.
   deleteNode(uuid: UUID) {
-    const node = this.nodes.get(uuid);
-    // delete outgoing edges
-    for (const [label, toId] of node.outgoing.entries()) {
-      const toNode = this.nodes.get(toId);
-      // remove edge from toNode.incoming
-      for (const [i, {label, fromId}] of toNode.incoming.entries()) {
-        toNode.incoming.splice(i, 1);
-        break;
+    const node = this.nodes.getOptional(uuid);
+    if (node !== undefined) {
+      // delete outgoing edges
+      for (const [label, targetId] of node.outgoing.entries()) {
+        const targetNode = this.nodes.get(targetId);
+        // remove edge from targetNode.incoming
+        const i = this.lookupIncoming(targetNode, label, uuid);
+        targetNode.incoming.splice(i, 1);
       }
+      // delete incoming edges
+      for (const {label, srcId} of node.incoming) {
+        const srcNode = this.nodes.get(srcId);
+        // remove edge from srcNode.outgoing
+        srcNode.outgoing.delete(label);
+      }
+      // delete node itself
+      this.nodes.delete(uuid);      
     }
-    // delete incoming edges
-    for (const {label, fromId} of node.incoming) {
-      const fromNode = this.nodes.get(fromId);
-      // remove edge from fromNode.outgoing
-      fromNode.outgoing.delete(label);
-    }
-    // delete node itself
-    this.nodes.delete(uuid);
   }
 
-  createEdge(fromId: NodeId, toId: NodeId, label: string) {
-    const fromNode = this.nodes.get(fromId);
-    fromNode.outgoing.set(label, toId);
+  // Set a node's outgoing edge to point to a node
+  // Idempotent.
+  setEdge(srcId: NodeId, label: string, targetId: NodeId) {
+    // gotta remove the existing edge first, if it exists
+    this.unsetEdge(srcId, label);
 
-    const toNode = this.nodes.get(toId);
-    toNode.incoming.push({label, fromId});
+    const srcNode = this.nodes.get(srcId);
+    srcNode.outgoing.set(label, targetId);
+    const targetNode = this.nodes.get(targetId);
+    targetNode.incoming.push({label, srcId});
   }
 
-  deleteEdge(nodeId: NodeId, label: string) {
-    const node = this.nodes.get(nodeId);
-    const toId = node.outgoing.get(label);
-    const toNode = this.nodes.get(toId);
-    for (const [i, {label, fromId}] of toNode.incoming.entries()) {
-      if (label === label && nodeIdsEqual(nodeId, fromId)) {
-        toNode.incoming.splice(i, 1);
-        break;
+  // Unset, i.e., delete an edge
+  // Idempotent.
+  unsetEdge(srcId: NodeId, label: string) {
+    const srcNode = this.nodes.get(srcId);
+    const existingTargetId = srcNode.outgoing.get(label);
+    if (existingTargetId !== undefined) {
+      // remove the respective entry in the existingTargetNode's 'incoming' array:
+      const existingTargetNode = this.nodes.get(existingTargetId);
+      const i = this.lookupIncoming(existingTargetNode, label, srcId);
+      existingTargetNode.incoming.splice(i, 1); // remove from array
+    }
+    srcNode.outgoing.delete(label);
+  }
+
+  // In a node's array of incoming edges, search for {label, srcId}, and return the position in the array.
+  private lookupIncoming(node: Node, label: string, srcId: NodeId): number {
+    for (const [i, {label: l, srcId: s}] of node.incoming.entries()) {
+      if (l === label && nodeIdsEqual(s, srcId)) {
+        return i;
       }
     }
-    node.outgoing.delete(label);
+    throw new Error("Not found!");
   }
 
-  readEdge(fromId: NodeId, label: string): NodeId {
-    // TODO: write test for this function
-    const fromNode = this.nodes.get(fromId);
-    return fromNode.outgoing.get(label);
+  // Read a node's outgoing edge with given label.
+  // If no such edge exists, returns undefined.
+  readEdge(srcId: NodeId, label: string): NodeId | undefined {
+    const srcNode = this.nodes.getOptional(srcId);
+    if (srcNode !== undefined) {
+      return srcNode.outgoing.get(label);
+    }
   }
 
   // Get all the edges in the graph. Slow. For debugging purposes.
   getEdges(): Array<[NodeId, NodeId, string]> {
-    const result = [];
+    const result: Array<[NodeId, NodeId, string]> = [];
     // get all outgoing edges of ordinary nodes
-    for (const [fromId, fromNode] of this.nodes.nodes.entries()) {
-      for (const [label, toId] of fromNode.outgoing) {
-        result.push([new UUID(fromId), toId, label]);
+    for (const [srcId, srcNode] of this.nodes.nodes.entries()) {
+      for (const [label, targetId] of srcNode.outgoing) {
+        result.push([new UUID(srcId), targetId, label]);
       }
     }
     // get all outgoing edges of value nodes
-    for (const [fromId, fromNode] of this.nodes.values.entries()) {
-      for (const [label, toId] of fromNode.outgoing) {
-        result.push([fromId, toId, label]);
+    for (const [srcId, srcNode] of this.nodes.values.entries()) {
+      for (const [label, targetId] of srcNode.outgoing) {
+        result.push([srcId, targetId, label]);
       }
     }
     return result;

+ 4 - 2
package.json

@@ -1,7 +1,8 @@
 {
 	"devDependencies": {
 		"@types/mocha": "^9.1.1",
-		"mocha": "^10.0.0"
+		"mocha": "^10.0.0",
+		"nyc": "^15.1.0"
 	},
 	"dependencies": {
 		"@types/node": "^18.6.1",
@@ -9,6 +10,7 @@
 		"typescript": "^4.7.4"
 	},
 	"scripts": {
-		"test": "mocha --require ts-node/register './*.test.ts'"
+		"test": "mocha --require ts-node/register './*.test.ts'",
+		"test-with-coverage": "nyc --reporter=text mocha --require ts-node/register './*.test.ts'"
 	}
 }

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 881 - 3
pnpm-lock.yaml


+ 2 - 1
tsconfig.json

@@ -1,6 +1,7 @@
 {
   "compilerOptions": {
     "types": ["mocha", "node"],
-    "target": "es6"
+    "target": "es6",
+    "strictNullChecks": true
   }
 }