diff --git a/lib/testing/assert.fnl b/lib/testing/assert.fnl index becbf5f..3bd7478 100644 --- a/lib/testing/assert.fnl +++ b/lib/testing/assert.fnl @@ -4,6 +4,10 @@ [actual expected message] (assert (= actual expected) (.. message " instead got " (hs.inspect actual)))) +(fn exports.not-eq? + [first second message] + (assert (not= first second) (.. message " instead both were " (hs.inspect first)))) + (fn exports.ok? [actual message] (assert (= (not (not actual)) true) (.. message " instead got " (hs.inspect actual)))) diff --git a/test/new-statemachine-test.fnl b/test/new-statemachine-test.fnl new file mode 100644 index 0000000..f4a55c8 --- /dev/null +++ b/test/new-statemachine-test.fnl @@ -0,0 +1,113 @@ +(local is (require :lib.testing.assert)) +(local statemachine (require :lib.new-statemachine)) +(local atom (require :lib.atom)) + +(fn make-fsm + [] + (statemachine.new + ;; States that the machine can be in mapped to their actions and transitions + {:state {:current-state :closed + :context {:i 0 + :event nil}} + + :states {:closed {:toggle (fn closed->opened + [state action extra] + {:state {:current-state :opened + :context {:i (+ state.context.i 1)}} + :effect :opening})} + :opened {:toggle (fn opened->closed + [state action extra] + {:state {:current-state :closed + :context {:i (+ state.context.i 1)}} + :effect :closing})}}})) + +(describe + "State Machine" + (fn [] + + (it "Should create a new fsm in the closed state" + (fn [] + (let [fsm (make-fsm)] + (is.eq? (. (atom.deref fsm.state) :current-state) :closed "Initial state was not closed")))) + + (it "Should include some methods" + (fn [] + (let [fsm (make-fsm)] + (is.eq? (type fsm.get-state) :function "No get-state method") + (is.eq? (type fsm.signal) :function "No get-state method") + (is.eq? (type fsm.subscribe) :function "No get-state method")))) + + (it "Should transition to opened on toggle action" + (fn [] + (let [fsm (make-fsm)] + (is.eq? (fsm.signal :toggle) true "Dispatch did not return true for handled event") + (is.eq? (. (atom.deref fsm.state) :current-state) :opened "State did not transition to opened")))) + + (it "Should transition from closed -> opened -> closed" + (fn [] + (let [fsm (make-fsm)] + (fsm.signal :toggle) + (fsm.signal :toggle) + (is.eq? (. (atom.deref fsm.state) :current-state) :closed "State did not transition back to closed") + (is.eq? (. (atom.deref fsm.state) :context :i) 2 "context.i should be 2 from 2 transitions")))) + + (it "Should not explode when dispatching an unhandled event" + (fn [] + (let [fsm (make-fsm)] + (is.eq? (fsm.signal :fail nil) false "The FSM exploded from dispatching a :fail event")))) + + (it "Subscribers should be called on events" + (fn [] + (let [fsm (make-fsm) + i (atom.new 0)] + (fsm.subscribe (fn [] (atom.swap! i (fn [v] (+ v 1))))) + (fsm.signal :toggle) + (is.eq? (atom.deref i) 1 "The subscriber was not called")))) + + (it "Subscribers should be provided old and new context, action, effect, and extra" + (fn [] + (let [fsm (make-fsm) + _old (atom.new nil) + _new (atom.new nil) + _action (atom.new nil) + _effect (atom.new nil) + _extra (atom.new nil)] + (fsm.subscribe (fn [{: prev-state : next-state : action : effect : extra}] + (atom.swap! _old (fn [_ nv] nv) prev-state) + (atom.swap! _new (fn [_ nv] nv) next-state) + (atom.swap! _action (fn [_ nv] nv) action) + (atom.swap! _effect (fn [_ nv] nv) effect) + (atom.swap! _extra (fn [_ nv] nv) extra))) + (fsm.signal :toggle :extra) + (is.not-eq? (. (atom.deref _old) :context :i) + (. (atom.deref _new) :context :i) "Subscriber did not get old and new state") + (is.eq? (atom.deref _action) :toggle "Subscriber did not get correct action") + (is.eq? (atom.deref _effect) :opening "Subscriber did not get correct effect") + (is.eq? (atom.deref _extra) :extra "Subscriber did not get correct extra")))) + + (it "Subscribers should be able to unsubscribe" + (fn [] + (let [fsm (make-fsm)] + (let [i (atom.new 0) + unsub (fsm.subscribe (fn [] (atom.swap! i (fn [v] (+ v 1)))))] + (fsm.signal :toggle) + (unsub) + (fsm.signal :toggle) + (is.eq? (atom.deref i) 1 "The subscriber was called after unsubscribing"))))) + + (it "Effect handler should maintain cleanup function" + (fn [] + (let [fsm (make-fsm) + effect-state (atom.new :unused) + unsub (fsm.subscribe (statemachine.effect-handler + {:opening (fn [] + (atom.swap! effect-state (fn [_ nv] nv) :opened) + ; Returned cleanup func + (fn [] + (atom.swap! effect-state (fn [_ nv] nv) :cleaned) + ))}))] + (fsm.signal :toggle) + (is.eq? (atom.deref effect-state) :opened "Effect handler should have been called") + (fsm.signal :toggle) + (is.eq? (atom.deref effect-state) :cleaned "Cleanup function should have been called") + )))))