packages feed

distributed-process-fsm-0.0.1: tests/TestFSM.hs

{-# LANGUAGE ExistentialQuantification  #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveGeneric              #-}

module Main where

import Control.Distributed.Process hiding (call, send, sendChan)
import Control.Distributed.Process.UnsafePrimitives (send, sendChan)
import Control.Distributed.Process.Node
import Control.Distributed.Process.Extras
 ( ExitReason(..)
 , isProcessAlive
 )
import qualified Control.Distributed.Process.Extras (__remoteTable)
import Control.Distributed.Process.Extras.Time hiding (timeout)
import Control.Distributed.Process.Extras.Timer
import Control.Distributed.Process.FSM hiding (State, liftIO)
import Control.Distributed.Process.FSM.Client (call, callTimeout)
import Control.Distributed.Process.SysTest.Utils
import Control.Monad (replicateM_, forM_)
import Control.Rematch (equalTo)

#if ! MIN_VERSION_base(4,6,0)
import Prelude hiding (catch, drop)
#else
import Prelude hiding (drop, (*>))
#endif

import Test.Framework as TF (defaultMain, testGroup, Test)
import Test.Framework.Providers.HUnit

import Network.Transport.TCP
import qualified Network.Transport as NT

-- import Control.Distributed.Process.Serializable (Serializable)
-- import Control.Monad (void)
import Data.Binary (Binary)
import Data.Maybe (fromJust)
import Data.Typeable (Typeable)
import GHC.Generics

data State = On | Off deriving (Eq, Show, Typeable, Generic)
instance Binary State where

data Reset = Reset deriving (Eq, Show, Typeable, Generic)
instance Binary Reset where

data Check = Check deriving (Eq, Show, Typeable, Generic)
instance Binary Check where

type StateData = Integer
type ButtonPush = ()
type Stop = ExitReason

initCount :: StateData
initCount = 0

startState :: Step State Integer
startState = initState Off initCount

waitForDown :: MonitorRef -> Process DiedReason
waitForDown ref =
  receiveWait [ matchIf (\(ProcessMonitorNotification ref' _ _) -> ref == ref')
                        (\(ProcessMonitorNotification _ _ dr) -> return dr) ]

switchFsm :: Step State StateData
switchFsm = startState
         ^. ((event :: Event ButtonPush)
              ~> (  (On  ~@ (set_ (+1) >> enter Off)) -- on => off => on is possible with |> here...
                 .| (Off ~@ (set_ (+1) >> enter On))
                 ) |> (reply currentState))
         .| ((event :: Event Stop)
              ~> (  ((== ExitNormal) ~? (\_ -> timeout (seconds 3) Reset))
                    {- let's verify that we can't override a normal shutdown sequence... -}
                 .| ((== ExitShutdown) ~? (\_ -> timeout (seconds 3) Reset))
                 .| ((const True) ~? stop)
                 ))
         .| ((event :: Event Check) ~> reply stateData)
         .| (event :: Event Reset)
              ~> (allState $ \Reset -> put initCount >> enter Off)

pushButton :: ProcessId -> Process State
pushButton pid = call pid (() :: ButtonPush)

check :: ProcessId -> Process StateData
check pid = call pid Check

switchFsmAlt :: Step State StateData
switchFsmAlt =
  begin startState $
    pick (await (event :: Event ButtonPush) ((pick (atState On  (set_ (+1) >> enter Off))
                                                   (atState Off (set_ (+1) >> enter On))) `join` (reply currentState)))
         (pick (await (event :: Event Stop) (pick (matching (== ExitNormal) (\_ -> timeout (seconds 3) Reset))
                                                  (matching (const True) stop)))
               (pick (await (event :: Event Check) (reply stateData))
                     (await (event :: Event Reset) (always $ \Reset -> put initCount >> enter Off))))

blockingFsm :: SendPort () -> Step State ()
blockingFsm sp = initState Off ()
            ^.  ((event :: Event ())
                *> (allState $ \() -> (lift $ sleep (seconds 10) >> sendChan sp ()) >> resume))
             .| ((event :: Event Stop)
                ~> (  ((== ExitNormal)   ~? (\_ -> resume) )
                      {- let's verify that we can't override
                         a normal shutdown sequence... -}
                   .| ((== ExitShutdown) ~? const resume)
                   ))

deepFSM :: SendPort () -> SendPort () -> Step State ()
deepFSM on off = initState Off ()
       ^. ((event :: Event State) ~> (allState $ \s -> enter s))
       .| ( (whenStateIs Off)
            |> (  ((event :: Event ())
                    ~> (allState $ \s -> (lift $ sendChan off s) >> resume))
               .| (((event :: Event String) ~> (always $ \(_ :: String) -> resume))
                    |> (reply (currentInput >>= return . fromJust :: FSM State () String)))
               )
          )
       .| ( (On ~@ resume) -- equivalent to `whenStateIs On`
            |> ((event :: Event ())
                ~> (allState $ \s -> (lift $ sendChan on s) >> resume))
          )

genFSM :: SendPort () -> Step State ()
genFSM sp = initState Off ()
       ^. ( (whenStateIs Off)
            |> ((event :: Event ()) ~> (always $ \() -> postpone))
          )
       .| ( (((pevent 100) :: Event State) ~> (always $ \state -> enter state))
         .| ((event :: Event ()) ~> (always $ \() -> (lift $ sendChan sp ()) >> resume))
          )
       .| ( (event :: Event String)
             ~> ( (Off ~@ putBack)
               .| (On  ~@ (nextEvent ()))
                )
          )

republicationOfEvents :: Process ()
republicationOfEvents = do
  (sp, rp) <- newChan

  pid <- start Off () $ genFSM sp

  replicateM_ 15 $ send pid ()

  Nothing <- receiveChanTimeout (asTimeout $ seconds 5) rp

  send pid On

  replicateM_ 15 $ receiveChan rp

  send pid "hello"  -- triggers `nextEvent ()`

  res <- receiveChanTimeout (asTimeout $ seconds 5) rp :: Process (Maybe ())
  res `shouldBe` equalTo (Just ())

  send pid Off

  forM_ ([1..50] :: [Int]) $ \i -> send pid i
  send pid "yo"
  send pid On

  res' <- receiveChanTimeout (asTimeout $ seconds 20) rp :: Process (Maybe ())
  res' `shouldBe` equalTo (Just ())

  kill pid "thankyou byebye"

verifyOuterStateHandler :: Process ()
verifyOuterStateHandler = do
  (spOn, rpOn) <- newChan
  (spOff, rpOff) <- newChan

  pid <- start Off () $ deepFSM spOn spOff

  send pid On
  send pid ()
  Nothing <- receiveChanTimeout (asTimeout $ seconds 3) rpOff
  () <- receiveChan rpOn

  resp <- callTimeout pid "hello there" (seconds 3):: Process (Maybe String)
  resp `shouldBe` equalTo (Nothing :: Maybe String)

  send pid Off
  send pid ()
  Nothing <- receiveChanTimeout (asTimeout $ seconds 3) rpOn
  () <- receiveChan rpOff

  res <- call pid "hello" :: Process String
  res `shouldBe` equalTo "hello"

  kill pid "bye bye"

verifyMailboxHandling :: Process ()
verifyMailboxHandling = do
  (sp, rp) <- newChan :: Process (SendPort (), ReceivePort ())
  pid <- start Off () (blockingFsm sp)

  send pid ()
  exit pid ExitNormal

  sleep $ seconds 5
  alive <- isProcessAlive pid
  alive `shouldBe` equalTo True

  -- we should resume after the ExitNormal handler runs, and get back into the ()
  -- handler due to safeWait (*>) which adds a `safe` filter check for the given type
  () <- receiveChan rp

  exit pid ExitShutdown
  monitor pid >>= waitForDown
  alive' <- isProcessAlive pid
  alive' `shouldBe` equalTo False

verifyStopBehaviour :: Process ()
verifyStopBehaviour = do
  pid <- start Off initCount switchFsm
  alive <- isProcessAlive pid
  alive `shouldBe` equalTo True

  exit pid $ ExitOther "foobar"
  monitor pid >>= waitForDown
  alive' <- isProcessAlive pid
  alive' `shouldBe` equalTo False

notSoQuirkyDefinitions :: Process ()
notSoQuirkyDefinitions = do
  start Off initCount switchFsmAlt >>= walkingAnFsmTree

quirkyOperators :: Process ()
quirkyOperators = do
  start Off initCount switchFsm >>= walkingAnFsmTree

walkingAnFsmTree :: ProcessId -> Process ()
walkingAnFsmTree pid = do
  mSt <- pushButton pid
  mSt `shouldBe` equalTo On

  mSt' <- pushButton pid
  mSt' `shouldBe` equalTo Off

  mCk <- check pid
  mCk `shouldBe` equalTo (2 :: StateData)

  -- verify that the process implementation turns exit signals into handlers...
  exit pid ExitNormal
  sleep $ seconds 6
  alive <- isProcessAlive pid
  alive `shouldBe` equalTo True

  mCk2 <- check pid
  mCk2 `shouldBe` equalTo (0 :: StateData)

  mrst' <- pushButton pid
  mrst' `shouldBe` equalTo On

  exit pid ExitShutdown
  monitor pid >>= waitForDown
  alive' <- isProcessAlive pid
  alive' `shouldBe` equalTo False

myRemoteTable :: RemoteTable
myRemoteTable =
  Control.Distributed.Process.Extras.__remoteTable $  initRemoteTable

tests :: NT.Transport  -> IO [Test]
tests transport = do
  {- verboseCheckWithResult stdArgs -}
  localNode <- newLocalNode transport myRemoteTable
  return [
        testGroup "Language/DSL"
        [
          testCase "Traversing an FSM definition (operators)"
           (runProcess localNode quirkyOperators)
        , testCase "Traversing an FSM definition (functions)"
           (runProcess localNode notSoQuirkyDefinitions)
        , testCase "Traversing an FSM definition (exit handling)"
           (runProcess localNode verifyStopBehaviour)
        , testCase "Traversing an FSM definition (mailbox handling)"
           (runProcess localNode verifyMailboxHandling)
        , testCase "Traversing an FSM definition (nested definitions)"
           (runProcess localNode verifyOuterStateHandler)
        , testCase "Traversing an FSM definition (event re-publication)"
           (runProcess localNode republicationOfEvents)
        ]
    ]

main :: IO ()
main = testMain $ tests

-- | Given a @builder@ function, make and run a test suite on a single transport
testMain :: (NT.Transport -> IO [Test]) -> IO ()
testMain builder = do
  Right (transport, _) <- createTransportExposeInternals
                                    "127.0.0.1" "10501" defaultTCPParameters
  testData <- builder transport
  defaultMain testData