-- | -- Module: -- Reflex.Test.Monad.Host -- Description: -- This module contains a monad for testing reflex networks module Reflex.Test.Monad.Host ( TestGuestT , TestGuestConstraints , ReflexTriggerRef , MonadReflexTest(..) , AppState(..) , ReflexTestT(..) , runReflexTestT , ReflexTestApp(..) , runReflexTestApp ) where import Prelude import Control.Concurrent.Chan import Control.Monad.IO.Class import Control.Monad.Fix import Control.Monad.Primitive import Control.Monad.Reader import Control.Monad.Ref import Control.Monad.State.Strict import Data.Dependent.Sum import Data.Functor.Identity import Data.Kind import Reflex import Reflex.Class () import Reflex.Host.Class type TestGuestT t (m :: Type -> Type) = TriggerEventT t (PostBuildT t (PerformEventT t m)) type TestGuestConstraints t (m :: Type -> Type) = ( MonadReflexHost t m , MonadHold t m , MonadSample t m , Ref m ~ Ref IO , MonadRef m , MonadRef (HostFrame t) , Ref (HostFrame t) ~ Ref IO , MonadIO (HostFrame t) , PrimMonad (HostFrame t) , MonadIO m , MonadFix m ) -- | since we work with this type directly a lot, it helps to wrap it around a type synonym type ReflexTriggerRef t (m :: Type -> Type) a = Ref m (Maybe (EventTrigger t a)) -- | class MonadReflexTest t m | m -> t where -- | since event subscriptions also happen within the monad, input triggers created via 'newEventWithTriggerRef' may be stuck in the 'Nothing' state as there are no listeners yet -- therefore it's necessary to pass in IORefs to the EventTriggers, thus the name of this type -- in practice, this will likely be a record containing many trigger refs and the monad user must deref them all type InputTriggerRefs m :: Type -- | in practice, this will likely be a record containing events and behaviors for the monad user to build a 'ReadPhase' that is passed into 'fireQueuedEventsAndRead' type OutputEvents m :: Type -- | the inner monad that reflex is running in -- likely 'SpiderHost Global' type InnerMonad m :: Type -> Type -- | see comments for 'InputTriggerRefs' inputTriggerRefs :: m (InputTriggerRefs m) -- | all queued triggers will fire simultaneous on the next execution of 'fireQueuedEventsAndRead' queueEventTrigger :: DSum (EventTrigger t) Identity -> m () -- | same as 'queueEventTrigger' except works with trigger refs -- if the trigger ref derefs to 'Nothing', the event does not get queued queueEventTriggerRef :: Ref (InnerMonad m) (Maybe (EventTrigger t a)) -> a -> m () -- | see comments for 'OutputEvents' outputs :: m (OutputEvents m) -- | fire all queued events and run a ReadPhase to produce results from the execution frames -- readphase takes place in the inner monad fireQueuedEventsAndRead :: ReadPhase (InnerMonad m) a -> m [a] -- | same as above with no ReadPhase fireQueuedEvents :: (Monad (ReadPhase (InnerMonad m))) => m [()] fireQueuedEvents = fireQueuedEventsAndRead (return ()) -- m is 'InnerMonad' from above data AppState t m = AppState { _appState_queuedEvents :: [DSum (EventTrigger t) Identity] -- ^ events to fire in next 'FireCommand' -- ^ 'FireCommand' to fire events and run next frame , _appState_fire :: FireCommand t m -- ^ 'FireCommand' to fire events and run next frame } -- | implementation of 'MonadReflexTest' newtype ReflexTestT t intref out m a = ReflexTestT { unReflexTestM :: ReaderT (intref, out) (StateT (AppState t m) m) a } deriving ( Functor , Applicative , Monad , MonadIO , MonadFix , MonadReader (intref, out) , MonadState (AppState t m)) deriving instance MonadSample t m => MonadSample t (ReflexTestT t intref out m) deriving instance MonadHold t m => MonadHold t (ReflexTestT t intref out m) deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ReflexTestT t intref out m) instance MonadTrans (ReflexTestT t intref out) where lift = ReflexTestT . lift . lift instance (MonadSubscribeEvent t m) => MonadSubscribeEvent t (ReflexTestT t intref out m) where subscribeEvent = lift . subscribeEvent instance (MonadRef m) => MonadReflexTest t (ReflexTestT t intref out m) where type InputTriggerRefs (ReflexTestT t intref out m) = intref type OutputEvents (ReflexTestT t intref out m) = out type InnerMonad (ReflexTestT t intref out m) = m inputTriggerRefs = do (intref,_) <- ask return intref queueEventTrigger evt = do as <- get put $ as { _appState_queuedEvents = evt : _appState_queuedEvents as } queueEventTriggerRef ref a = do mpulse <- lift $ readRef ref case mpulse of Nothing -> return () Just pulse -> do as <- get put $ as { _appState_queuedEvents = (pulse :=> Identity a) : _appState_queuedEvents as } outputs = do (_,out) <- ask return out fireQueuedEventsAndRead rp = do as <- get put $ as { _appState_queuedEvents = [] } lift $ (runFireCommand $ _appState_fire as) (_appState_queuedEvents as) rp runReflexTestT :: forall intref inev out t m a . (TestGuestConstraints t m) => (inev, intref) -- ^ make sure intref match inev, i.e. return values of newEventWithTriggerRef -> (inev -> TestGuestT t m out) -- ^ network to test -> ReflexTestT t intref out m a -- ^ test monad to run -> m () runReflexTestT (input, inputTRefs) app rtm = do (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef events <- liftIO newChan (output, fc@(FireCommand fire)) <- do hostPerformEventT $ flip runPostBuildT postBuild $ flip runTriggerEventT events $ app input -- handle post build -- TODO consider adding some way to test 'PostBuild' results mPostBuildTrigger <- readRef postBuildTriggerRef _ <- case mPostBuildTrigger of Nothing -> return [()] -- no subscribers Just postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return () -- TODO maybe find a way to handle trigger events -- one solution is to implement non-blocking variant of TriggerEventT -- and then pass as part of AppState such that each call to readPhase will fire any trigger events -- another option is just to start a thread and output warnings anytime triggerEvs are created --triggerEvs <- liftIO $ readChan events -- run the test monad flip runStateT (AppState [] fc) $ flip runReaderT (inputTRefs, output) $ unReflexTestM rtm return () -- | class to help bind network and types to a 'ReflexTestT' -- see test/Reflex/Test/Monad/HostSpec.hs for usage example class ReflexTestApp app t m | app -> t m where data AppInputTriggerRefs app :: Type data AppInputEvents app :: Type data AppOutput app :: Type getApp :: AppInputEvents app -> TestGuestT t m (AppOutput app) makeInputs :: m (AppInputEvents app, AppInputTriggerRefs app) runReflexTestApp :: (ReflexTestApp app t m, TestGuestConstraints t m) => ReflexTestT t (AppInputTriggerRefs app) (AppOutput app) m () -> m () runReflexTestApp rtm = do i <- makeInputs runReflexTestT i getApp rtm