Test at "server agent" level

This commit is contained in:
Nicholas Kariniemi 2014-09-25 21:58:18 +03:00
parent edab2ad684
commit 39307f5a73
4 changed files with 189 additions and 113 deletions

View file

@ -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]

View file

@ -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})))

View file

@ -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)))))

View file

@ -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?))))