From 6baf8c3c485181cb37fa491f7b6bfb8b37adbd5c Mon Sep 17 00:00:00 2001 From: gered Date: Fri, 27 May 2016 15:29:42 -0400 Subject: [PATCH] add refresh hint tests --- test/views/hint_tests.clj | 276 ++++++++++++++++++++++++++++++++++++ test/views/test_helpers.clj | 13 ++ 2 files changed, 289 insertions(+) create mode 100644 test/views/hint_tests.clj diff --git a/test/views/hint_tests.clj b/test/views/hint_tests.clj new file mode 100644 index 0000000..b98c1fe --- /dev/null +++ b/test/views/hint_tests.clj @@ -0,0 +1,276 @@ +(ns views.hint-tests + (:use + clojure.test + views.test-helpers + views.protocols + views.core + views.test-memory-db)) + + +(def test-sent-data + (atom [])) + +(defn test-send-fn [subscriber-key [view-sig view-data]] + (swap! test-sent-data conj {:subscriber-key subscriber-key + :view-sig view-sig + :view-data view-data})) + + +; fixtures + +(defn reset-system-fixture [f] + (reset! view-system {}) + (reset! statistics {}) + (f) + (shutdown! true)) + +(defn clear-sent-data-fixture [f] + (reset! test-sent-data []) + (f)) + +(use-fixtures :each clear-sent-data-fixture reset-system-fixture reset-memory-db-fixture) + + + +;; tests + +(deftest refresh-views!-instantly-attempts-view-refresh-with-given-hints + (let [options default-options + hints-refreshed (atom [])] + ; 1. init views + (init! views test-send-fn options) + ; with a view subscription (any subscription will do) + (with-redefs [subscribed-views (fn [] #{(->view-sig :namespace :fake-view [])}) + refresh-view! (fn [hints _] (swap! hints-refreshed into hints))] + ; 2. trigger refresh by calling refresh-views! with hints + (refresh-views! [(hint :namespace [:foo] :fake-type)]) + (is (contains-only? @hints-refreshed + [(hint :namespace [:foo] :fake-type)])) + (reset! hints-refreshed []) + ; 3. same thing again, but passing in multiple hints + (refresh-views! [(hint :namespace [:foo] :fake-type) + (hint :namespace [:bar] :fake-type)]) + (is (contains-only? @hints-refreshed + [(hint :namespace [:foo] :fake-type) + (hint :namespace [:bar] :fake-type)])) + (reset! hints-refreshed [])) + ; now, without any view subscriptions + (with-redefs [subscribed-views (fn [] #{}) + refresh-view! (fn [hints _] (swap! hints-refreshed into hints))] + ; 4. again trigger refresh by calling refresh-views! with hints + (refresh-views! [(hint :namespace [:foo] :fake-type)]) + (is (empty? @hints-refreshed)) + (reset! hints-refreshed [])))) + +(deftest refresh-watcher-runs-at-specified-interval-and-picks-up-queued-hints + (let [options default-options + hints-refreshed (atom [])] + (with-redefs [subscribed-views (fn [] #{(->view-sig :namespace :fake-view [])}) + refresh-view! (fn [hints _] (swap! hints-refreshed into hints))] + ; 1. init views + (init! views test-send-fn options) + ; 2. queue a hint and wait until the next refresh interval + (queue-hints! [(hint :namespace [:foo] :fake-type)]) + (wait-for-refresh-interval options) + (is (contains-only? @hints-refreshed + [(hint :namespace [:foo] :fake-type)])) + (reset! hints-refreshed []) + ; 3. queue multiple hints and wait again + (queue-hints! [(hint :namespace [:foo] :fake-type) + (hint :namespace [:bar] :fake-type)]) + (wait-for-refresh-interval options) + (is (contains-only? @hints-refreshed + [(hint :namespace [:foo] :fake-type) + (hint :namespace [:bar] :fake-type)])) + (reset! hints-refreshed []) + ; 4. queue up no hints and wait + (wait-for-refresh-interval options) + (reset! hints-refreshed [])))) + +(deftest refresh-worker-thread-processes-relevant-hints + (let [options default-options + views-refreshed (atom [])] + ; 1. init views + (init! views test-send-fn options) + (with-redefs [subscribed-views (fn [] #{(->view-sig :a :foo [])}) + do-view-refresh! (fn [view-sig] (swap! views-refreshed into [view-sig]))] + ; 2. trigger refresh by calling refresh-views! with relevant hint + (refresh-views! [(hint :a [:foo] memory-view-hint-type)]) + (wait-for-refresh-views) + (is (contains-only? @views-refreshed [(->view-sig :a :foo [])])) + (reset! views-refreshed []) + ; 3. same thing again, but passing in multiple hints (1 relevant, 1 not) + (refresh-views! [(hint :a [:foo] memory-view-hint-type) + (hint :a [:bar] memory-view-hint-type)]) + (wait-for-refresh-views) + (is (contains-only? @views-refreshed [(->view-sig :a :foo [])])) + (reset! views-refreshed []) + ; 4. and lastly, passing in only irrelevant hints + (refresh-views! [(hint :b [:foo] memory-view-hint-type) + (hint :a [:foo] :some-other-type)]) + (wait-for-refresh-views) + (is (empty? @views-refreshed)) + (reset! views-refreshed [])))) + +; this test is really just testing that our helper function memory-db-assoc-in! works as we expect it to +; (otherwise, it is entirely redundant given the above tests) +(deftest test-memory-db-operation-triggers-proper-refresh-hints + (let [options default-options + hints-refreshed (atom []) + views-refreshed (atom [])] + ; 1. init views + (init! views test-send-fn options) + ; first tests verifying that correct hints are being sent out (don't care if relevant or not yet) + (with-redefs [subscribed-views (fn [] #{(->view-sig :a :foo [])}) + refresh-view! (fn [hints _] (swap! hints-refreshed into hints))] + (memory-db-assoc-in! :a [:foo] 42) + (memory-db-assoc-in! :a [:bar] 3.14) + (memory-db-assoc-in! :b [:baz] [10 20 30]) + (wait-for-refresh-views) + (is (contains-only? @hints-refreshed + [(hint :a [:foo] memory-view-hint-type) + (hint :a [:bar] memory-view-hint-type) + (hint :b [:baz] memory-view-hint-type)])) + (reset! views-refreshed [])) + ; now we test that relevant views were recognized as relevant and forwarded on to be used to + ; trigger actual refreshes of view data + (with-redefs [subscribed-views (fn [] #{(->view-sig :a :foo [])}) + do-view-refresh! (fn [view-sig] (swap! views-refreshed into [view-sig]))] + ; 2. update memory database (in a location covered by the subscribed view) + (memory-db-assoc-in! :a [:foo] 1337) + (wait-for-refresh-interval options) + (is (contains-only? @views-refreshed [(->view-sig :a :foo [])])) + (reset! views-refreshed []) + ; 3. same thing again, but update a different location not covered by any subscription + (memory-db-assoc-in! :a [:bar] 1234.5678) + (wait-for-refresh-interval options) + (is (empty? @views-refreshed))))) + +(deftest relevant-hints-cause-refreshed-data-to-be-sent-to-subscriber + (let [options default-options + subscriber-key 123 + view-sig (->view-sig :a :foo [])] + ; 1. init views + (init! views test-send-fn options) + ; 2. subscribe to a view + (let [original-view-data (get-view-data view-sig) + updated-view-data 21 + subscribe-result (subscribe! view-sig subscriber-key nil)] + ; 3. block until subscription finishes. we don't care about the initial view data refresh + (while (not (realized? subscribe-result))) + (reset! test-sent-data []) + (is (= (hash original-view-data) (get-in @view-system [:hashes view-sig]))) + ; 4. change some test data that is covered by the view subscription + (memory-db-assoc-in! :a [:foo] updated-view-data) + (wait-for-refresh-views) + (is (= (hash updated-view-data) (get-in @view-system [:hashes view-sig]))) + (is (contains-only? @test-sent-data + [{:subscriber-key subscriber-key + :view-sig (dissoc view-sig :namespace) + :view-data updated-view-data}]))))) + +(deftest irrelevant-hints-dont-trigger-refreshes + (let [options default-options + subscriber-key 123 + view-sig (->view-sig :a :foo [])] + ; 1. init views + (init! views test-send-fn options) + ; 2. subscribe to a view + (let [subscribe-result (subscribe! view-sig subscriber-key nil)] + ; 3. block until subscription finishes. we don't care about the initial view data refresh + (while (not (realized? subscribe-result))) + (reset! test-sent-data []) + ; 4. change some test data that is NOT covered by the view subscription + (memory-db-assoc-in! :b [:foo] 6) + (memory-db-assoc-in! :a [:bar] 7) + (wait-for-refresh-views) + (is (empty? @test-sent-data))))) + +(deftest refreshes-not-sent-if-view-data-is-unchanged-since-last-refresh + (let [options default-options + subscriber-key 123 + view-sig (->view-sig :a :foo [])] + ; 1. init views + (init! views test-send-fn options) + ; 2. subscribe to a view + (let [updated-view-data 1111 + subscribe-result (subscribe! view-sig subscriber-key nil)] + ; 3. block until subscription finishes. we don't care about the initial view data refresh + (while (not (realized? subscribe-result))) + (reset! test-sent-data []) + ; 4. change some test data, will cause a refresh to be sent out + (memory-db-assoc-in! :a [:foo] updated-view-data) + (wait-for-refresh-views) + (is (= (hash updated-view-data) (get-in @view-system [:hashes view-sig]))) + (is (contains-only? @test-sent-data + [{:subscriber-key subscriber-key + :view-sig (dissoc view-sig :namespace) + :view-data updated-view-data}])) + (reset! test-sent-data []) + ; 5. manually trigger another refresh for the view + (refresh-views! [(hint :a [:foo] memory-view-hint-type)]) + (wait-for-refresh-views) + (is (empty? @test-sent-data)) + ; 6. also try "updating" the db with the same values + (memory-db-assoc-in! :a [:foo] updated-view-data) + (wait-for-refresh-views) + (is (empty? @test-sent-data))))) + +(deftest refresh-queue-drops-duplicate-hints + (let [options (-> default-options + ; enable statistics collection + (assoc :stats-log-interval 10000)) + subscriber-key 123 + view-sig (->view-sig :a :foo [])] + ; 1. init views + (init! views test-send-fn options) + ; 2. prematurely stop refresh worker threads so that we can more easily inspect the + ; internal refresh queue's entries. the refresh worker threads are what remove + ; hints from the refresh queue as they are added to it. + (stop-refresh-worker-threads) + ; 3. subscribe to a view + (let [subscribe-result (subscribe! view-sig subscriber-key nil)] + ; 4. block until subscription finishes + (while (not (realized? subscribe-result))) + (is (= 0 (:deduplicated @statistics))) + ; 5. add duplicate hints by changing the same set of data twice + ; (hints will stay in the queue forever because we stopped the worker threads) + (memory-db-assoc-in! :a [:foo] 6) + (memory-db-assoc-in! :a [:foo] 7) + (wait-for-refresh-views) + (is (= 1 (:deduplicated @statistics))) + (is (= [view-sig] + (vec (:refresh-queue @view-system))))))) + +(deftest refresh-queue-drops-hints-when-full + (let [options (-> default-options + ; enable statistics collection + (assoc :stats-log-interval 10000 + :refresh-queue-size 1)) + subscriber-key 123 + view-sig-a (->view-sig :a :foo []) + view-sig-b (->view-sig :b :foo [])] + ; 1. init views + (init! views test-send-fn options) + ; 2. prematurely stop refresh worker threads so that we can more easily inspect the + ; internal refresh queue's entries. the refresh worker threads are what remove + ; hints from the refresh queue as they are added to it. + (stop-refresh-worker-threads) + ; 3. subscribe to a view + ; note: log* redef is to suppress error log output which will normally happen whenever + ; another item is added to the refresh queue when it's already full + (with-redefs [clojure.tools.logging/log* (fn [& _])] + (let [subscribe-a (subscribe! view-sig-a subscriber-key nil) + subscribe-b (subscribe! view-sig-b subscriber-key nil)] + ; 4. block until subscription finishes + (while (or (not (realized? subscribe-a)) + (not (realized? subscribe-b)))) + (is (= 0 (:dropped @statistics))) + ; 5. change some data affecting the subscribed view, resulting in more then 1 hint + ; being added to the refresh queue + (memory-db-assoc-in! :a [:foo] 101010) + (memory-db-assoc-in! :b [:foo] 010101) + (wait-for-refresh-views) + (is (= 1 (:dropped @statistics))) + (is (= [view-sig-a] + (vec (:refresh-queue @view-system)))))))) diff --git a/test/views/test_helpers.clj b/test/views/test_helpers.clj index b459c0e..9a82577 100644 --- a/test/views/test_helpers.clj +++ b/test/views/test_helpers.clj @@ -26,3 +26,16 @@ (data (get-in @view-system [:views (:view-id view-sig)]) (:namespace view-sig) (:parameters view-sig))) + +(defn wait-for-refresh-views [] + (Thread/sleep 200)) + +(defn wait-for-refresh-interval [options] + (Thread/sleep (+ 200 (:refresh-interval options)))) + +(defn stop-refresh-worker-threads [] + (swap! view-system assoc :stop-workers? true) + (doseq [^Thread t (:workers @view-system)] + (.interrupt t) + (.join t)) + (swap! view-system assoc :workers nil))