diff web/graph_test.coffee @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/web/graph_test.coffee@3c997bc6d380
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/graph_test.coffee	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,257 @@
+log = console.log
+assert = require('chai').assert
+expect = require('chai').expect
+SyncedGraph = require('./graph.js').SyncedGraph
+
+describe 'SyncedGraph', ->
+  describe 'constructor', ->
+    it 'should successfully make an empty graph without connecting to rdfdb', ->
+      g = new SyncedGraph()
+      g.quads()
+      assert.equal(g.quads().length, 0)
+
+  describe 'auto dependencies', ->
+    graph = new SyncedGraph()
+    RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+    U = (tail) -> graph.Uri('http://example.com/' + tail)
+    A1 = U('a1')
+    A2 = U('a2')
+    A3 = U('a3')
+    A4 = U('a4')
+    ctx = U('ctx')
+    quad = (s, p, o) -> graph.Quad(s, p, o, ctx)
+
+    beforeEach (done) ->
+      graph = new SyncedGraph()
+      graph.loadTrig("
+        @prefix : <http://example.com/> .
+        :ctx {
+          :a1 :a2 :a3 .
+          :a1 :someFloat 1.5 .
+          :a1 :someString \"hello\" .
+          :a1 :multipleObjects :a4, :a5 .
+          :a2 a :Type1 .
+          :a3 a :Type1 .
+        }
+      ", done)
+    
+    it 'calls a handler right away', ->
+      called = 0
+      hand = ->
+        called++
+      graph.runHandler(hand, 'run')
+      assert.equal(1, called)
+      
+    it 'calls a handler a 2nd time if the graph is patched with relevant data', ->
+      called = 0
+      hand = ->
+        called++
+        graph.uriValue(A1, A2)
+      graph.runHandler(hand, 'run')
+      graph.applyAndSendPatch({
+        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
+      assert.equal(2, called)
+
+    it 'notices new queries a handler makes upon rerun', ->
+      called = 0
+      objsFound = []
+      hand = ->
+        called++
+        graph.uriValue(A1, A2)
+        if called > 1
+          objsFound.push(graph.objects(A1, A3))
+      graph.runHandler(hand, 'run')
+      # first run looked up A1,A2,*
+      graph.applyAndSendPatch({
+        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
+      # second run also looked up A1,A3,* (which matched none)
+      graph.applyAndSendPatch({
+        delQuads: [], addQuads: [quad(A1, A3, A4)]})
+      # third run should happen here, noticing the new A1,A3,* match
+      assert.equal(3, called)
+      assert.deepEqual([[], [A4]], objsFound)
+
+    it 'calls a handler again even if the handler throws an error', ->
+      called = 0
+      hand = ->
+        called++
+        graph.uriValue(A1, A2)
+        throw new Error('this test handler throws an error')
+      graph.runHandler(hand, 'run')
+      graph.applyAndSendPatch({
+        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
+      assert.equal(2, called)
+
+    describe 'works with nested handlers', ->
+
+      innerResults = []
+      inner = ->
+        console.log('\nninnerfetch')
+        innerResults.push(graph.uriValue(A1, A2))
+        console.log("innerResults #{JSON.stringify(innerResults)}\n")
+
+      outerResults = []
+      doRunInner = true
+      outer = ->
+        if doRunInner
+          graph.runHandler(inner, 'runinner')
+        console.log('push outer')
+        outerResults.push(graph.floatValue(A1, U('someFloat')))
+
+      beforeEach ->
+        innerResults = []
+        outerResults = []
+        doRunInner = true
+
+      affectInner = {
+        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]
+      }
+      affectOuter = {
+        delQuads: [
+          quad(A1, U('someFloat'), graph.Literal(1.5))
+        ], addQuads: [
+          quad(A1, U('someFloat'), graph.LiteralRoundedFloat(2))
+        ]}
+      affectBoth = {
+        delQuads: affectInner.delQuads.concat(affectOuter.delQuads),
+        addQuads: affectInner.addQuads.concat(affectOuter.addQuads)
+        }
+                  
+      it 'calls everything normally once', ->
+        graph.runHandler(outer, 'run')
+        assert.deepEqual([A3], innerResults)
+        assert.deepEqual([1.5], outerResults)
+
+      it.skip '[performance] reruns just the inner if its dependencies change', ->
+        console.log(graph.quads())
+        graph.runHandler(outer, 'run')
+        graph.applyAndSendPatch(affectInner)
+        assert.deepEqual([A3, A4], innerResults)
+        assert.deepEqual([1.5], outerResults)
+        
+      it.skip '[performance] reruns the outer (and therefore inner) if its dependencies change', ->
+        graph.runHandler(outer, 'run')
+        graph.applyAndSendPatch(affectOuter)
+        assert.deepEqual([A3, A3], innerResults)
+        assert.deepEqual([1.5, 2], outerResults)
+        
+        
+      it.skip '[performance] does not send a redundant inner run if it is already rerunning outer', ->
+        # Note that outer may or may not call inner each time, and we
+        # don't want to redundantly call inner. We need to:
+        #  1. build the set of handlers to rerun,
+        #  2. call them from outside-in, and
+        #  3. any runHandler calls that happen, they need to count as reruns.
+        graph.runHandler(outer, 'run')
+        graph.applyAndSendPatch(affectBoth)
+        assert.deepEqual([A3, A4], innerResults)
+        assert.deepEqual([1.5, 2], outerResults)
+
+      it 'reruns the outer and the inner if all dependencies change, but outer omits calling inner this time', ->
+        graph.runHandler(outer, 'run')
+        doRunInner = false
+        graph.applyAndSendPatch(affectBoth)
+        assert.deepEqual([A3, A4], innerResults)
+        assert.deepEqual([1.5, 2], outerResults)
+        
+    describe 'watches calls to:', ->
+      it 'floatValue', ->
+        values = []
+        hand = -> values.push(graph.floatValue(A1, U('someFloat')))
+        graph.runHandler(hand, 'run')
+        graph.patchObject(A1, U('someFloat'), graph.LiteralRoundedFloat(2), ctx)
+        assert.deepEqual([1.5, 2.0], values)
+        
+      it 'stringValue', ->
+        values = []
+        hand = -> values.push(graph.stringValue(A1, U('someString')))
+        graph.runHandler(hand, 'run')
+        graph.patchObject(A1, U('someString'), graph.Literal('world'), ctx)
+        assert.deepEqual(['hello', 'world'], values)
+
+      it 'uriValue', ->
+        # covered above, but this one tests patchObject on a uri, too
+        values = []
+        hand = -> values.push(graph.uriValue(A1, A2))
+        graph.runHandler(hand, 'run')
+        graph.patchObject(A1, A2, A4, ctx)
+        assert.deepEqual([A3, A4], values)
+
+      it 'objects', ->
+        values = []
+        hand = -> values.push(graph.objects(A1, U('multipleObjects')))
+        graph.runHandler(hand, 'run')
+        graph.patchObject(A1, U('multipleObjects'), U('newOne'), ctx)
+        expect(values[0]).to.deep.have.members([U('a4'), U('a5')])
+        expect(values[1]).to.deep.have.members([U('newOne')])
+
+      it 'subjects', ->
+        values = []
+        rdfType = graph.Uri(RDF + 'type')
+        hand = -> values.push(graph.subjects(rdfType, U('Type1')))
+        graph.runHandler(hand, 'run')
+        graph.applyAndSendPatch(
+          {delQuads: [], addQuads: [quad(A4, rdfType, U('Type1'))]})
+        expect(values[0]).to.deep.have.members([A2, A3])
+        expect(values[1]).to.deep.have.members([A2, A3, A4])
+
+      describe 'items', ->
+        it 'when the list order changes', (done) ->
+          values = []
+          successes = 0
+          hand = ->
+            try
+              head = graph.uriValue(U('x'), U('y'))
+            catch
+              # graph goes empty between clearGraph and loadTrig
+              return
+            values.push(graph.items(head))
+            successes++
+          graph.clearGraph()
+          graph.loadTrig "
+            @prefix : <http://example.com/> .
+            :ctx { :x :y (:a1 :a2 :a3) } .
+          ", () ->
+             graph.runHandler(hand, 'run')
+             graph.clearGraph()
+             graph.loadTrig "
+              @prefix : <http://example.com/> .
+              :ctx { :x :y (:a1 :a3 :a2) } .
+            ", () ->
+               assert.deepEqual([[A1, A2, A3], [A1, A3, A2]], values)
+               assert.equal(2, successes)
+               done()
+  
+      describe 'contains', ->
+        it 'when a new triple is added', ->
+          values = []
+          hand = -> values.push(graph.contains(A1, A1, A1))
+          graph.runHandler(hand, 'run')
+          graph.applyAndSendPatch(
+            {delQuads: [], addQuads: [quad(A1, A1, A1)]})
+          assert.deepEqual([false, true], values)
+
+        it 'when a relevant triple is removed', ->
+          values = []
+          hand = -> values.push(graph.contains(A1, A2, A3))
+          graph.runHandler(hand, 'run')
+          graph.applyAndSendPatch(
+            {delQuads: [quad(A1, A2, A3)], addQuads: []})
+          assert.deepEqual([true, false], values)
+
+    describe 'performs well', ->
+      it "[performance] doesn't call handler a 2nd time if the graph gets an unrelated patch", ->
+        called = 0
+        hand = ->
+          called++
+          graph.uriValue(A1, A2)
+        graph.runHandler(hand, 'run')
+        graph.applyAndSendPatch({
+          delQuads: [], addQuads: [quad(A2, A3, A4)]})
+        assert.equal(1, called)
+
+      it.skip '[performance] calls a handler 2x but then not again if the handler stopped caring about the data', ->
+        assert.fail()
+
+      it.skip "[performance] doesn't get slow if the handler makes tons of repetitive lookups", ->
+        assert.fail()