diff --git a/light9/web/graph_test.coffee b/light9/web/graph_test.coffee --- a/light9/web/graph_test.coffee +++ b/light9/web/graph_test.coffee @@ -1,4 +1,6 @@ +log = console.log assert = require('chai').assert +expect = require('chai').expect SyncedGraph = require('./graph.js').SyncedGraph describe 'SyncedGraph', -> @@ -8,3 +10,244 @@ describe '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) -> {subject: s, predicate: p, object: o, graph: ctx} + + beforeEach (done) -> + graph = new SyncedGraph() + graph.loadTrig(" + @prefix : . + :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) + 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) + 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) + # 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) + graph.applyAndSendPatch({ + delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]}) + assert.equal(2, called) + + describe 'works with nested handlers', -> + + innerResults = [] + inner = -> + innerResults.push(graph.uriValue(A1, A2)) + + outerResults = [] + doRunInner = true + outer = -> + if doRunInner + graph.runHandler(inner) + 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'), '"1.5"^^http://www.w3.org/2001/XMLSchema#decimal') + ], 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) + assert.deepEqual([A3], innerResults) + assert.deepEqual([1.5], outerResults) + + it '[performance] reruns just the inner if its dependencies change', -> + graph.runHandler(outer) + graph.applyAndSendPatch(affectInner) + assert.deepEqual([A3, A4], innerResults) + assert.deepEqual([1.5], outerResults) + + it '[performance] reruns the outer (and therefore inner) if its dependencies change', -> + graph.runHandler(outer) + graph.applyAndSendPatch(affectOuter) + assert.deepEqual([A3, A3], innerResults) + assert.deepEqual([1.5, 2], outerResults) + + + it '[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) + 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) + 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) + 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) + 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) + 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) + 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) + 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 : . + :ctx { :x :y (:a1 :a2 :a3) } . + ", () -> + graph.runHandler(hand) + graph.clearGraph() + graph.loadTrig " + @prefix : . + :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) + 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) + 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) + graph.applyAndSendPatch({ + delQuads: [], addQuads: [quad(A2, A3, A4)]}) + assert.equal(1, called) + + it '[performance] calls a handler 2x but then not again if the handler stopped caring about the data', -> + assert.fail() + + it "[performance] doesn't get slow if the handler makes tons of repetitive lookups", -> + assert.fail()