308 lines
8.7 KiB
Clojure
308 lines
8.7 KiB
Clojure
|
|
(ns com.stuartsierra.component-test
|
||
|
|
(:require [clojure.test :refer (deftest is are)]
|
||
|
|
[clojure.set :refer (map-invert)]
|
||
|
|
[com.stuartsierra.component :as component]))
|
||
|
|
|
||
|
|
(def ^:dynamic *log* nil)
|
||
|
|
|
||
|
|
(defn- log [& args]
|
||
|
|
(when (thread-bound? #'*log*)
|
||
|
|
(set! *log* (conj *log* args))))
|
||
|
|
|
||
|
|
(defn- ordering
|
||
|
|
"Given an ordered collection of messages, returns a map from the
|
||
|
|
head of each message to its index position in the collection."
|
||
|
|
[log]
|
||
|
|
(into {} (map-indexed (fn [i [message & _]] [message i]) log)))
|
||
|
|
|
||
|
|
(defn before?
|
||
|
|
"In the collection of messages, does the message beginning with
|
||
|
|
symbol a come before the message begging with symbol b?"
|
||
|
|
[log sym-a sym-b]
|
||
|
|
(let [order (ordering log)]
|
||
|
|
(< (get order sym-a) (get order sym-b))))
|
||
|
|
|
||
|
|
(defn started? [component]
|
||
|
|
(true? (::started? component)))
|
||
|
|
|
||
|
|
(defn stopped? [component]
|
||
|
|
(false? (::started? component)))
|
||
|
|
|
||
|
|
(defrecord ComponentA [state]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'ComponentA.start this)
|
||
|
|
(assoc this ::started? true))
|
||
|
|
(stop [this]
|
||
|
|
(log 'ComponentA.stop this)
|
||
|
|
(assoc this ::started? false)))
|
||
|
|
|
||
|
|
(defn component-a []
|
||
|
|
(->ComponentA (rand-int Integer/MAX_VALUE)))
|
||
|
|
|
||
|
|
(defrecord ComponentB [state a]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'ComponentB.start this)
|
||
|
|
(assert (started? a))
|
||
|
|
(assoc this ::started? true))
|
||
|
|
(stop [this]
|
||
|
|
(log 'ComponentB.stop this)
|
||
|
|
(assert (started? a))
|
||
|
|
(assoc this ::started? false)))
|
||
|
|
|
||
|
|
(defn component-b []
|
||
|
|
(component/using
|
||
|
|
(map->ComponentB {:state (rand-int Integer/MAX_VALUE)})
|
||
|
|
[:a]))
|
||
|
|
|
||
|
|
(defrecord ComponentC [state a b]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'ComponentC.start this)
|
||
|
|
(assert (started? a))
|
||
|
|
(assert (started? b))
|
||
|
|
(assoc this ::started? true))
|
||
|
|
(stop [this]
|
||
|
|
(log 'ComponentC.stop this)
|
||
|
|
(assert (started? a))
|
||
|
|
(assert (started? b))
|
||
|
|
(assoc this ::started? false)))
|
||
|
|
|
||
|
|
(defn component-c []
|
||
|
|
(component/using
|
||
|
|
(map->ComponentC {:state (rand-int Integer/MAX_VALUE)})
|
||
|
|
[:a :b]))
|
||
|
|
|
||
|
|
(defrecord ComponentD [state my-c b]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'ComponentD.start this)
|
||
|
|
(assert (started? b))
|
||
|
|
(assert (started? my-c))
|
||
|
|
(assoc this ::started? true))
|
||
|
|
(stop [this]
|
||
|
|
(log 'ComponentD.stop this)
|
||
|
|
(assert (started? b))
|
||
|
|
(assert (started? my-c))
|
||
|
|
(assoc this ::started? false)))
|
||
|
|
|
||
|
|
(defn component-d []
|
||
|
|
(map->ComponentD {:state (rand-int Integer/MAX_VALUE)}))
|
||
|
|
|
||
|
|
(defrecord ComponentE [state]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'ComponentE.start this)
|
||
|
|
(assoc this ::started? true))
|
||
|
|
(stop [this]
|
||
|
|
(log 'ComponentE.stop this)
|
||
|
|
(assoc this ::started? false)))
|
||
|
|
|
||
|
|
(defn component-e []
|
||
|
|
(map->ComponentE {:state (rand-int Integer/MAX_VALUE)}))
|
||
|
|
|
||
|
|
(defrecord System1 [d a e c b] ; deliberately scrambled order
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(log 'System1.start this)
|
||
|
|
(component/start-system this))
|
||
|
|
(stop [this]
|
||
|
|
(log 'System1.stop this)
|
||
|
|
(component/stop-system this)))
|
||
|
|
|
||
|
|
(defn system-1 []
|
||
|
|
(map->System1 {:a (component-a)
|
||
|
|
:b (component-b)
|
||
|
|
:c (component-c)
|
||
|
|
:d (component/using (component-d)
|
||
|
|
{:b :b
|
||
|
|
:my-c :c})
|
||
|
|
:e (component-e)}))
|
||
|
|
|
||
|
|
(defmacro with-log [& body]
|
||
|
|
`(binding [*log* []]
|
||
|
|
~@body
|
||
|
|
*log*))
|
||
|
|
|
||
|
|
(deftest components-start-in-order
|
||
|
|
(let [log (with-log (component/start (system-1)))]
|
||
|
|
(are [k1 k2] (before? log k1 k2)
|
||
|
|
'ComponentA.start 'ComponentB.start
|
||
|
|
'ComponentA.start 'ComponentC.start
|
||
|
|
'ComponentB.start 'ComponentC.start
|
||
|
|
'ComponentC.start 'ComponentD.start
|
||
|
|
'ComponentB.start 'ComponentD.start)))
|
||
|
|
|
||
|
|
(deftest all-components-started
|
||
|
|
(let [system (component/start (system-1))]
|
||
|
|
(doseq [component (vals system)]
|
||
|
|
(is (started? component)))))
|
||
|
|
|
||
|
|
(deftest all-components-stopped
|
||
|
|
(let [system (component/stop (component/start (system-1)))]
|
||
|
|
(doseq [component (vals system)]
|
||
|
|
(is (stopped? component)))))
|
||
|
|
|
||
|
|
(deftest dependencies-satisfied
|
||
|
|
(let [system (component/start (component/start (system-1)))]
|
||
|
|
(are [keys] (started? (get-in system keys))
|
||
|
|
[:b :a]
|
||
|
|
[:c :a]
|
||
|
|
[:c :b]
|
||
|
|
[:d :my-c])))
|
||
|
|
|
||
|
|
(defrecord ErrorStartComponentC [state error a b]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(throw error))
|
||
|
|
(stop [this]
|
||
|
|
this))
|
||
|
|
|
||
|
|
(defn error-start-c [error]
|
||
|
|
(component/using
|
||
|
|
(map->ErrorStartComponentC {:error error})
|
||
|
|
[:a :b]))
|
||
|
|
|
||
|
|
(defn setup-error
|
||
|
|
([]
|
||
|
|
(setup-error (ex-info "Boom!" {})))
|
||
|
|
([error]
|
||
|
|
(try (component/start
|
||
|
|
(assoc (system-1) :c (error-start-c error)))
|
||
|
|
(catch Exception e e))))
|
||
|
|
|
||
|
|
(deftest error-thrown-with-partial-system
|
||
|
|
(let [ex (setup-error)]
|
||
|
|
(is (started? (-> ex ex-data :system :b :a)))))
|
||
|
|
|
||
|
|
(deftest error-thrown-with-component-dependencies
|
||
|
|
(let [ex (setup-error)]
|
||
|
|
(is (started? (-> ex ex-data :component :a)))
|
||
|
|
(is (started? (-> ex ex-data :component :b)))))
|
||
|
|
|
||
|
|
(deftest error-thrown-with-cause
|
||
|
|
(let [error (ex-info "Boom!" {})
|
||
|
|
ex (setup-error error)]
|
||
|
|
(is (identical? error (.getCause ^Exception ex)))))
|
||
|
|
|
||
|
|
(deftest error-is-from-component
|
||
|
|
(let [error (ex-info "Boom!" {})
|
||
|
|
ex (setup-error error)]
|
||
|
|
(is (component/ex-component? ex))))
|
||
|
|
|
||
|
|
(deftest error-is-not-from-component
|
||
|
|
(is (not (component/ex-component? (ex-info "Boom!" {})))))
|
||
|
|
|
||
|
|
(deftest remove-components-from-error
|
||
|
|
(let [error (ex-info (str (rand-int Integer/MAX_VALUE)) {})
|
||
|
|
^Exception ex (setup-error error)
|
||
|
|
^Exception ex-without (component/ex-without-components ex)]
|
||
|
|
(is (contains? (ex-data ex) :component))
|
||
|
|
(is (contains? (ex-data ex) :system))
|
||
|
|
(is (not (contains? (ex-data ex-without) :component)))
|
||
|
|
(is (not (contains? (ex-data ex-without) :system)))
|
||
|
|
(is (= (.getMessage ex)
|
||
|
|
(.getMessage ex-without)))
|
||
|
|
(is (= (.getCause ex)
|
||
|
|
(.getCause ex-without)))
|
||
|
|
(is (java.util.Arrays/equals
|
||
|
|
(.getStackTrace ex)
|
||
|
|
(.getStackTrace ex-without)))))
|
||
|
|
|
||
|
|
(defrecord System2b [one]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
(assert (started? (get-in one [:b :a])))
|
||
|
|
this)
|
||
|
|
(stop [this]
|
||
|
|
(assert (started? (get-in one [:b :a])))
|
||
|
|
this))
|
||
|
|
|
||
|
|
(defn system-2 []
|
||
|
|
(component/system-map :alpha (system-1)
|
||
|
|
:beta (component/using (->System2b nil)
|
||
|
|
{:one :alpha})))
|
||
|
|
|
||
|
|
(deftest composed-systems
|
||
|
|
(let [system (component/start (system-2))]
|
||
|
|
(is (started? (get-in system [:beta :one :d :my-c])))))
|
||
|
|
|
||
|
|
(defn increment-all-components [system]
|
||
|
|
(component/update-system
|
||
|
|
system (keys system) update-in [:n] inc))
|
||
|
|
|
||
|
|
(defn assert-increments [system]
|
||
|
|
(are [n keys] (= n (get-in system keys))
|
||
|
|
11 [:a :n]
|
||
|
|
11 [:b :a :n]
|
||
|
|
11 [:c :a :n]
|
||
|
|
11 [:c :b :a :n]
|
||
|
|
11 [:e :d :b :a :n]
|
||
|
|
21 [:b :n]
|
||
|
|
21 [:c :b :n]
|
||
|
|
21 [:d :b :n]
|
||
|
|
31 [:c :n]
|
||
|
|
41 [:d :n]
|
||
|
|
51 [:e :n]))
|
||
|
|
|
||
|
|
(deftest update-with-custom-function-on-maps
|
||
|
|
(let [system {:a {:n 10}
|
||
|
|
:b (component/using {:n 20} [:a])
|
||
|
|
:c (component/using {:n 30} [:a :b])
|
||
|
|
:d (component/using {:n 40} [:a :b])
|
||
|
|
:e (component/using {:n 50} [:b :c :d])}]
|
||
|
|
(assert-increments (increment-all-components system))))
|
||
|
|
|
||
|
|
(deftest t-system-using
|
||
|
|
(let [dependency-map {:b [:a]
|
||
|
|
:c [:a :b]
|
||
|
|
:d {:a :a :b :b}
|
||
|
|
:e [:b :c :d]}
|
||
|
|
system {:a {:n 10}
|
||
|
|
:b {:n 20}
|
||
|
|
:c {:n 30}
|
||
|
|
:d {:n 40}
|
||
|
|
:e {:n 50}}
|
||
|
|
system (component/system-using system dependency-map)]
|
||
|
|
(assert-increments (increment-all-components system))))
|
||
|
|
|
||
|
|
(defrecord ComponentWithoutLifecycle [state])
|
||
|
|
|
||
|
|
;; BB-TEST-PATCH: No implementation of method errors for start and stop
|
||
|
|
#_(deftest component-without-lifecycle
|
||
|
|
(let [c (->ComponentWithoutLifecycle nil)]
|
||
|
|
(is (= c (component/start c)))
|
||
|
|
(is (= c (component/stop c)))))
|
||
|
|
|
||
|
|
(defrecord ComponentReturningNil [state]
|
||
|
|
component/Lifecycle
|
||
|
|
(start [this]
|
||
|
|
nil)
|
||
|
|
(stop [this]
|
||
|
|
nil))
|
||
|
|
|
||
|
|
(deftest component-returning-nil
|
||
|
|
(let [a (->ComponentReturningNil nil)
|
||
|
|
s (component/system-map :a a :b (component-b))
|
||
|
|
e (try (component/start s)
|
||
|
|
false
|
||
|
|
(catch Exception e e))]
|
||
|
|
(is (= ::component/nil-component (:reason (ex-data e))))))
|
||
|
|
|
||
|
|
(deftest missing-dependency-error
|
||
|
|
(let [system-key ::system-b
|
||
|
|
local-key ::local-b
|
||
|
|
a (component/using (component-a) {local-key system-key})
|
||
|
|
system (component/system-map
|
||
|
|
:a a)
|
||
|
|
e (try (component/start system)
|
||
|
|
false
|
||
|
|
(catch Exception e e))
|
||
|
|
data (ex-data e)]
|
||
|
|
(is (= ::component/missing-dependency (:reason data)))
|
||
|
|
(is (= system-key (:system-key data)))
|
||
|
|
(is (= local-key (:dependency-key data)))
|
||
|
|
(is (= a (:component data)))
|
||
|
|
(is (= system (:system data)))))
|