Browse Source

Extend versioning history test.

Joeri Exelmans 4 years ago
parent
commit
4db1a5ce1c
2 changed files with 53 additions and 24 deletions
  1. 7 16
      lib/versioning/History.js
  2. 46 8
      lib/versioning/test_History.js

+ 7 - 16
lib/versioning/History.js

@@ -8,30 +8,20 @@ class Operation {
     this.value = value;
     this.parents = parents; // object mapping every key in value to a parent Operation
   }
-  getValue(key) {
-    return this.value[key];
-  }
-  // Basically omit forward references and replaces references by IDs.
-  // Result can be JSON'd.
+  // Basically replaces JS references by IDs.
+  // Result can be JSON'd with constant time+space complexity. Useful for sharing an edit over the network.
   serialize(op) {
-    const parentIds = {};
-    for (const [key, parent] of Object.entries(this.parents)) {
-      parentIds[key] = parent.id;
-    }
-
-    const result = {
+    return {
       id: this.id,
       value: this.value,
-      parentIds,
+      parentIds: Object.fromEntries(Object.entries(this.parents).map(([key,parent])=>[key,parent.id])),
     }
-    return result;
   }
 }
 
-// const INITIAL_OP = new Operation("0", {}, {}); // The parent of all parentless Operations. Root of all histories. Globally unique.
-
 class Context {
   constructor(fetchCallback) {
+    // Must be a function taking a single 'id' parameter, returning a Promise resolving to the serialized operation with the given id.
     this.fetchCallback = fetchCallback;
 
     // "Global" stuff. Operations have GUIDs but can also be shared between Histories. For instance, the 'initial' operation is the common root of all model histories. We could have put these things in a global variable, but that would make it more difficult to mock 'remoteness' (separate contexts) in tests.
@@ -170,7 +160,7 @@ class History {
               }
               // rollback
               this.heads.set(key, parent);
-              this.setState(key, parent.getValue(key));
+              this.setState(key, parent.value[key]);
             }
           };
           // Received operation wins conflict - state must be rolled back before executing it
@@ -199,6 +189,7 @@ class History {
     this.ops.set(op.id, op);
   }
 
+  // Shorthand
   async receiveAndMerge(serializedOp) {
     const op = await this.context.receiveOperation(serializedOp);
     this.autoMerge(op);

+ 46 - 8
lib/versioning/test_History.js

@@ -126,8 +126,8 @@ async function runTest(verbose) {
     }
   }
 
-  function throwErrorOnFetch() {
-    throw new AssertionError("Did not expect async fetch");
+  function noFetch() {
+    throw new AssertionError("Did not expect fetch");
   }
 
   {
@@ -135,8 +135,8 @@ async function runTest(verbose) {
 
 
     // Local and remote are just names for our histories.
-    const localContext = new Context(throwErrorOnFetch);
-    const remoteContext = new Context(throwErrorOnFetch);
+    const localContext = new Context(noFetch);
+    const remoteContext = new Context(noFetch);
 
     const {history: localHistory,  state: localState } = createHistory("local", localContext);
     const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
@@ -153,8 +153,8 @@ async function runTest(verbose) {
   {
     info("\nTest case: Concurrency with conflict\n")
 
-    const localContext = new Context(throwErrorOnFetch);
-    const remoteContext = new Context(throwErrorOnFetch);
+    const localContext = new Context(noFetch);
+    const remoteContext = new Context(noFetch);
 
     const {history: localHistory, state: localState} = createHistory("local", localContext);
     const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
@@ -171,8 +171,8 @@ async function runTest(verbose) {
   {
     info("\nTest case: Concurrency with conflict (2)\n")
 
-    const localContext = new Context(throwErrorOnFetch);
-    const remoteContext = new Context(throwErrorOnFetch);
+    const localContext = new Context(noFetch);
+    const remoteContext = new Context(noFetch);
 
     const {history: localHistory, state: localState} = createHistory("local", localContext);
     const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
@@ -195,6 +195,44 @@ async function runTest(verbose) {
 
     assert(deepEqual(localState, remoteState));
   }
+
+  {
+    info("\nTest case: Fetch\n")
+
+    const fetched = [];
+
+    async function fetchFromLocal(id) {
+      // console.log("fetching", id)
+      fetched.push(id);
+      return localContext.ops.get(id).then(op => op.serialize());
+    }
+
+    const localContext = new Context(noFetch);
+    const remoteContext = new Context(fetchFromLocal);
+
+    const {history: localHistory, state: localState} = createHistory("local", localContext);
+
+    const localOps = [
+      localHistory.new({geometry:1}),                       // [0] (no deps)
+      localHistory.new({geometry:2, style: 3}),             // [1], depends on [0]
+      localHistory.new({style: 4}),                         // [2], depends on [1]
+      localHistory.new({geometry: 5, style: 6, parent: 7}), // [3], depends on [1], [2]
+      localHistory.new({parent: 8}),                        // [4], depends on [3]
+      localHistory.new({terminal: 9}),                      // [5] (no deps)
+    ];
+
+    // when given [2], should fetch [1], then [0]
+    await remoteContext.receiveOperation(localOps[2].serialize());
+    assert(deepEqual(fetched, [localOps[1].id, localOps[0].id]));
+
+    // when given [5], should not fetch anything
+    await remoteContext.receiveOperation(localOps[5].serialize());
+    assert(deepEqual(fetched, [localOps[1].id, localOps[0].id]));
+
+    // when given [4], should fetch [3]. (already have [0-2] from previous step)
+    await remoteContext.receiveOperation(localOps[4].serialize());
+    assert(deepEqual(fetched, [localOps[1].id, localOps[0].id, localOps[3].id]));
+  }
 }
 
 runTest(/* verbose: */ true).then(() => {