Test at "server agent" level
This commit is contained in:
parent
edab2ad684
commit
39307f5a73
4 changed files with 189 additions and 113 deletions
|
@ -7,34 +7,36 @@
|
||||||
;; Server state
|
;; Server state
|
||||||
(def states (atom []))
|
(def states (atom []))
|
||||||
|
|
||||||
(defn make-server-agent [in >client states]
|
(defn make-server-agent
|
||||||
(a/go-loop [client-state sync/empty-state]
|
([in out states] (make-server-agent in out states sync/empty-state))
|
||||||
(when-let [msg (<! in)]
|
([in out states initial-client-state]
|
||||||
(condp = (:type msg)
|
(a/go-loop [client-state initial-client-state]
|
||||||
:diff
|
(when-let [msg (<! in)]
|
||||||
(let [{:keys [new-states new-shadow full-sync?]} (sync/apply-diff @states (:diff msg) (:hash msg))]
|
(condp = (:type msg)
|
||||||
(println "diff!")
|
:diff
|
||||||
(if full-sync?
|
(let [states* @states
|
||||||
(let [state (sync/get-current-state @states)]
|
shadow (sync/get-history-state states* (:hash msg))]
|
||||||
(println "state not found, full sync!")
|
(if shadow
|
||||||
(>! >client (message/full-sync state))
|
(let [new-states (sync/apply-diff states* (:diff msg))
|
||||||
(recur state))
|
new-shadow (diff/patch-state shadow (:diff msg))
|
||||||
(do (println "state found, just send changes")
|
{:keys [diff hash]} (sync/diff-states new-states new-shadow)]
|
||||||
(let [{:keys [diff hash]} (sync/diff-states new-states new-shadow)]
|
(reset! states new-states)
|
||||||
(reset! states new-states)
|
(>! out (message/diff-msg diff hash))
|
||||||
(>! >client (message/diff-msg diff hash))
|
(recur new-shadow))
|
||||||
(recur new-shadow)))))
|
(let [state (sync/get-current-state @states)]
|
||||||
:full-sync
|
(>! out (message/full-sync state))
|
||||||
(let [state (sync/get-current-state @states)]
|
(recur state))))
|
||||||
(println "got full sync, send full sync")
|
|
||||||
(>! >client (message/full-sync state))
|
:full-sync
|
||||||
(recur state))
|
(let [state (sync/get-current-state @states)]
|
||||||
:new-state
|
(>! out (message/full-sync state))
|
||||||
(let [{:keys [diff shadow-hash]} (sync/diff-states (:new-states msg) client-state)]
|
(recur state))
|
||||||
(println "new-state!")
|
|
||||||
(>! >client (message/diff-msg diff shadow-hash)))
|
:new-state
|
||||||
(do (println "Unknown message:" msg)
|
(let [{:keys [diff hash]} (sync/diff-states (:new-states msg) client-state)]
|
||||||
(recur client-state))))))
|
(>! out (message/diff-msg diff hash)))
|
||||||
|
(do (println "Unknown message:" msg)
|
||||||
|
(recur client-state)))))))
|
||||||
|
|
||||||
;; TODO: Remove watch, close up channels properly
|
;; TODO: Remove watch, close up channels properly
|
||||||
(defn sync-new-client! [>client <client]
|
(defn sync-new-client! [>client <client]
|
||||||
|
|
|
@ -28,13 +28,7 @@
|
||||||
{:hash (hasch/uuid shadow)
|
{:hash (hasch/uuid shadow)
|
||||||
:diff (diff/diff-states shadow state)}))
|
:diff (diff/diff-states shadow state)}))
|
||||||
|
|
||||||
(defn apply-diff [states diff shadow-hash]
|
(defn apply-diff [states diff]
|
||||||
(let [state (:state (first states))
|
(let [new-state (diff/patch-state (get-current-state states) diff)]
|
||||||
shadow (get-history-state states shadow-hash)]
|
(add-history-state states new-state)))
|
||||||
(if shadow
|
|
||||||
{:new-states (add-history-state states (diff/patch-state state diff))
|
|
||||||
:new-shadow (diff/patch-state shadow diff)
|
|
||||||
:full-sync? false}
|
|
||||||
{:new-states states
|
|
||||||
:new-shadow state
|
|
||||||
:full-sync? true})))
|
|
||||||
|
|
|
@ -4,29 +4,160 @@
|
||||||
[hasch.core :as hasch]
|
[hasch.core :as hasch]
|
||||||
[clojure.core.async :as a :refer [<!! >!! chan go]]))
|
[clojure.core.async :as a :refer [<!! >!! chan go]]))
|
||||||
|
|
||||||
(deftest single-diff
|
(defn hashed-states [& states]
|
||||||
;; Returns empty ACK diff
|
(->> states
|
||||||
(let [in (chan 1)
|
(map (fn [s] {:hash (hasch/uuid s)
|
||||||
>client (chan 1)
|
:state s}))
|
||||||
state {:grubs {"1" {:text "2 apples" :completed false}}
|
(into [])))
|
||||||
:recipes {}}
|
|
||||||
hash (hasch/uuid state)
|
(deftest diff-no-server-changes
|
||||||
states* [{:hash hash :state state}]
|
;; Returns empty ACK diff with hash of current state
|
||||||
states (atom states*)
|
;; when no server changes
|
||||||
diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
(let [states (hashed-states
|
||||||
diff-msg {:type :diff
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}})
|
||||||
:diff diff
|
states* (atom states)
|
||||||
:hash hash}
|
msg {:type :diff
|
||||||
server-agent (state/make-server-agent in >client states)]
|
:diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
||||||
(>!! in diff-msg)
|
:hash (:hash (first states))}
|
||||||
(let [diff-response (<!! >client)]
|
in (chan 1)
|
||||||
(is (= @states
|
out (chan 1)]
|
||||||
[{:hash #uuid "0cb7ae13-2523-52fa-aa79-4a6f2489cafd"
|
(state/make-server-agent in out states*)
|
||||||
:state {:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}}
|
(>!! in msg)
|
||||||
{:hash #uuid "166d7e23-5a7b-5101-8364-0d2c06b8d554"
|
(let [diff-response (<!! out)]
|
||||||
:state {:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}}}]))
|
(is (= (hashed-states
|
||||||
(is (= diff-response
|
{:grubs {"1" {:completed false, :text "2 apples"}}, :recipes {}}
|
||||||
{:type :diff
|
{:grubs {"1" {:completed true, :text "2 apples"}}, :recipes {}})
|
||||||
|
@states*))
|
||||||
|
(is (= {:type :diff
|
||||||
:diff {:grubs {:deleted #{}, :updated nil}
|
:diff {:grubs {:deleted #{}, :updated nil}
|
||||||
:recipes {:deleted #{}, :updated nil}}
|
:recipes {:deleted #{}, :updated nil}}
|
||||||
:hash #uuid "166d7e23-5a7b-5101-8364-0d2c06b8d554"})))))
|
:hash (:hash (last @states*))}
|
||||||
|
diff-response)))))
|
||||||
|
|
||||||
|
(deftest diff-server-changes
|
||||||
|
;; Returns diff with changes when server has changed
|
||||||
|
;; Client state fetched from history
|
||||||
|
(let [states (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
states* (atom states)
|
||||||
|
msg {:type :diff
|
||||||
|
:diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
||||||
|
:hash (:hash (first states))}
|
||||||
|
in (chan 1)
|
||||||
|
out (chan 1)]
|
||||||
|
(state/make-server-agent in out states*)
|
||||||
|
(>!! in msg)
|
||||||
|
(let [diff-response (<!! out)]
|
||||||
|
(is (= (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed true}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
@states*))
|
||||||
|
(is (= {:type :diff
|
||||||
|
:diff {:grubs {:deleted #{}
|
||||||
|
:updated {"2" {:completed false, :text "3 onions"}}}
|
||||||
|
:recipes {:deleted #{}, :updated nil}}
|
||||||
|
:hash (hasch/uuid {:grubs {"1" {:text "2 apples" :completed true}}
|
||||||
|
:recipes {}})}
|
||||||
|
diff-response)))))
|
||||||
|
|
||||||
|
(deftest diff-client-out-of-sync
|
||||||
|
;; Returns full sync if client state not found
|
||||||
|
;; in history
|
||||||
|
(let [states (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
states* (atom states)
|
||||||
|
msg {:type :diff
|
||||||
|
:diff {:grubs {:updated {"0" {:completed true}} :deleted #{}}}
|
||||||
|
:hash (:hash {:grubs {"0" {:text "milk" :completed false}}
|
||||||
|
:recipes {}})}
|
||||||
|
in (chan 1)
|
||||||
|
out (chan 1)]
|
||||||
|
(state/make-server-agent in out states*)
|
||||||
|
(>!! in msg)
|
||||||
|
(let [diff-response (<!! out)]
|
||||||
|
(is (= (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
@states*))
|
||||||
|
(is (= {:type :full-sync
|
||||||
|
:state {:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}}
|
||||||
|
diff-response)))))
|
||||||
|
|
||||||
|
(deftest full-sync-request
|
||||||
|
;; Returns full sync if client requests it
|
||||||
|
(let [states (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
states* (atom states)
|
||||||
|
msg {:type :full-sync}
|
||||||
|
in (chan 1)
|
||||||
|
out (chan 1)]
|
||||||
|
(state/make-server-agent in out states*)
|
||||||
|
(>!! in msg)
|
||||||
|
(let [diff-response (<!! out)]
|
||||||
|
(is (= (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
@states*))
|
||||||
|
(is (= {:type :full-sync
|
||||||
|
:state {:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}}
|
||||||
|
diff-response)))))
|
||||||
|
|
||||||
|
(deftest new-state
|
||||||
|
;; Passes diff with new state to client
|
||||||
|
(let [states (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}
|
||||||
|
"3" {:text "milk" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
states* (atom states)
|
||||||
|
client-state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
msg {:type :new-state
|
||||||
|
:new-states states}
|
||||||
|
in (chan 1)
|
||||||
|
out (chan 1)]
|
||||||
|
(state/make-server-agent in out states* client-state)
|
||||||
|
(>!! in msg)
|
||||||
|
(let [diff-response (<!! out)]
|
||||||
|
(is (= (hashed-states
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}}
|
||||||
|
:recipes {}}
|
||||||
|
{:grubs {"1" {:text "2 apples" :completed false}
|
||||||
|
"2" {:text "3 onions" :completed false}
|
||||||
|
"3" {:text "milk" :completed false}}
|
||||||
|
:recipes {}})
|
||||||
|
@states*))
|
||||||
|
(is (= {:type :diff
|
||||||
|
:diff {:grubs {:deleted #{}
|
||||||
|
:updated {"2" {:text "3 onions" :completed false}
|
||||||
|
"3" {:text "milk" :completed false}}}
|
||||||
|
:recipes {:deleted #{}, :updated nil}}
|
||||||
|
:hash (hasch/uuid client-state)}
|
||||||
|
diff-response)))))
|
||||||
|
|
|
@ -25,54 +25,3 @@
|
||||||
(is (= {:a :b} (s/get-history-state states "hash1")))
|
(is (= {:a :b} (s/get-history-state states "hash1")))
|
||||||
(is (= {:c :d} (s/get-history-state states "hash2")))
|
(is (= {:c :d} (s/get-history-state states "hash2")))
|
||||||
(is (= {:e :f} (s/get-history-state states "hash3")))))
|
(is (= {:e :f} (s/get-history-state states "hash3")))))
|
||||||
|
|
||||||
(deftest apply-diff-no-changes
|
|
||||||
;; Apply changes and return ACK for in sync client/server
|
|
||||||
(let [state {:grubs {"1" {:text "2 apples" :completed false}}
|
|
||||||
:recipes {}}
|
|
||||||
hash (hasch/uuid state)
|
|
||||||
states [{:hash hash :state state}]
|
|
||||||
diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
|
||||||
shadow-hash hash
|
|
||||||
{:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)]
|
|
||||||
(do
|
|
||||||
(is (= {:grubs {"1" {:text "2 apples" :completed true}}
|
|
||||||
:recipes {}}
|
|
||||||
(:state (last new-states))))
|
|
||||||
(is (= {:grubs {"1" {:text "2 apples" :completed true}}
|
|
||||||
:recipes {}}
|
|
||||||
new-shadow))
|
|
||||||
(is (not full-sync?)))))
|
|
||||||
|
|
||||||
(deftest apply-diff-server-state-changed
|
|
||||||
;; Send differences back if server state changed
|
|
||||||
(let [state {:grubs {"1" {:text "3 apples" :completed false}} :recipes {}}
|
|
||||||
prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
states [{:hash (hasch/uuid state) :state state}
|
|
||||||
{:hash (hasch/uuid prev) :state prev}]
|
|
||||||
diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
|
||||||
shadow-hash (hasch/uuid prev)
|
|
||||||
{:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)]
|
|
||||||
(do
|
|
||||||
(is (= {:grubs {"1" {:text "3 apples" :completed true}}
|
|
||||||
:recipes {}}
|
|
||||||
(:state (last new-states))))
|
|
||||||
(is (= {:grubs {"1" {:text "2 apples" :completed true}}
|
|
||||||
:recipes {}}
|
|
||||||
new-shadow))
|
|
||||||
(is (not full-sync?)))))
|
|
||||||
|
|
||||||
(deftest apply-diff-client-out-of-sync
|
|
||||||
;; Shadow hash not in history means client has fallen too far
|
|
||||||
;; out of sync. Send a full sync
|
|
||||||
(let [state {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
prev {:grubs {"1" {:text "2 apples" :completed false}} :recipes {}}
|
|
||||||
states [{:hash (hasch/uuid state) :state state}
|
|
||||||
{:hash (hasch/uuid prev) :state prev}]
|
|
||||||
shadow-hash (hasch/uuid {:grubs {} :recipes {}})
|
|
||||||
diff {:grubs {:updated {"1" {:completed true}} :deleted #{}}}
|
|
||||||
{:keys [new-states new-shadow full-sync?]} (s/apply-diff states diff shadow-hash)]
|
|
||||||
(do
|
|
||||||
(is (= state (:state (last new-states))))
|
|
||||||
(is (= state new-shadow))
|
|
||||||
(is full-sync?))))
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue