packages feed

time-warp-0.1.1: test/Test/Control/TimeWarp/Timed/MonadTimedSpec.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE Rank2Types                #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TypeSynonymInstances      #-}
{-# LANGUAGE ViewPatterns              #-}

-- | RSCoin.Test.MonadTimed specification

module Test.Control.TimeWarp.Timed.MonadTimedSpec
       ( spec
       , timeoutProp -- workaround warning, remove after it's used
       ) where

import           Control.Concurrent.MVar      (newEmptyMVar, putMVar, takeMVar)
import           Control.Concurrent.STM       (atomically)
import           Control.Concurrent.STM.TVar  (newTVarIO, readTVarIO, writeTVar)
import           Control.Exception.Base       (Exception, SomeException)
import           Control.Monad                (void)
import           Control.Monad.Catch          (MonadCatch, catch, catchAll,
                                               handleAll, throwM)
import           Control.Monad.State          (StateT, execStateT, modify, put)
import           Control.Monad.Trans          (MonadIO, liftIO)
import           Data.Typeable                (Typeable)
import           Numeric.Natural              (Natural)
import           Test.Hspec                   (Spec, describe)
import           Test.Hspec.QuickCheck        (prop)
import           Test.QuickCheck              (NonNegative (..), Property,
                                               counterexample, ioProperty)
import           Test.QuickCheck.Function     (Fun, apply)
import           Test.QuickCheck.Monadic      (PropertyM, assert, monadic,
                                               monitor, run)
import           Test.QuickCheck.Poly         (A)

import           Control.TimeWarp.Timed       (Microsecond, MonadTimed (..),
                                               MonadTimedError, RelativeToNow,
                                               TimedIO, TimedT, TimedT, after,
                                               for, fork_, invoke, now,
                                               runTimedIO, runTimedT, schedule,
                                               sec, killThread)

import           Test.Control.TimeWarp.Common ()

spec :: Spec
spec =
    describe "MonadTimed" $ do
        monadTimedSpec "TimedIO" runTimedIOProp
        monadTimedTSpec "TimedT" runTimedTProp

monadTimedSpec
    :: (MonadTimed m, MonadIO m, MonadCatch m)
    => String
    -> (PropertyM m () -> Property)
    -> Spec
monadTimedSpec description runProp =
    describe description $ do
        describe "virtualTime >> virtualTime" $ do
            prop "first virtualTime will run before second virtualTime" $
                runProp virtualTimePassingProp
        describe "wait t" $ do
            prop "will wait at least t" $
                runProp . waitPassingProp
        describe "fork" $ do
            prop "won't change semantics of an action" $
                \a -> runProp . forkSemanticProp a
        describe "schedule" $ do
            prop "won't change semantics of an action, will execute action in the future" $
                \a b -> runProp . scheduleSemanticProp a b
        describe "invoke" $ do
            prop "won't change semantics of an action, will execute action in the future" $
                \a b -> runProp . invokeSemanticProp a b
-- TODO: fix tests for timeout in TimedIO.
--        describe "timeout" $ do
--            prop "should throw an exception if time has exceeded" $
--                \a -> runProp . timeoutProp a


monadTimedTSpec
    :: String
    -> (TimedTProp () -> Property)
    -> Spec
monadTimedTSpec description runProp =
    describe description $ do
        describe "virtualTime >> virtualTime" $ do
            prop "first virtualTime will run before second virtualTime" $
                runProp virtualTimePassingTimedProp
        describe "now" $ do
            prop "now is correct" $
                runProp . nowProp
        describe "wait t" $ do
            prop "will wait at least t" $
                runProp . waitPassingTimedProp
        describe "fork" $ do
            prop "won't change semantics of an action" $
                \a -> runProp . forkSemanticTimedProp a
        describe "schedule" $ do
            prop "won't change semantics of an action, will execute action in the future" $
                \a b -> runProp . scheduleSemanticTimedProp a b
        describe "invoke" $ do
            prop "won't change semantics of an action, will execute action in the future" $
                \a b -> runProp . invokeSemanticTimedProp a b
        describe "timeout" $ do
            prop "should throw an exception if time has exceeded" $
                \a -> runProp . timeoutTimedProp a
        describe "killThread" $ do
            prop "should abort the execution of a thread" $
                \a b -> runProp . killThreadTimedProp a b
        describe "exceptions" $ do
            prop "thrown nicely" $
                runProp exceptionsThrown
            prop "caught nicely" $
                runProp exceptionsThrowCaught
            prop "wait + throw caught nicely" $
                runProp exceptionsWaitThrowCaught
            prop "exceptions don't affect main thread" $
                runProp exceptionNotAffectMainThread
            prop "exceptions don't affect other threads" $
                runProp exceptionNotAffectOtherThread


-- pure version
-- type TimedTProp = TimedT (CatchT (State Bool))
type TimedTProp = TimedT (StateT Bool IO)

assertTimedT :: Bool -> TimedTProp ()
assertTimedT b = modify (b &&)

type RelativeToNowNat = Natural

fromIntegralRTN :: RelativeToNowNat -> RelativeToNow
fromIntegralRTN = (+) . fromIntegral

-- TODO: figure out how to test recursive functions like after/at

runTimedIOProp :: PropertyM TimedIO () -> Property
runTimedIOProp = monadic $ ioProperty . runTimedIO

runTimedTProp :: TimedTProp () -> Property
runTimedTProp test = ioProperty $ execStateT (runTimedT test) True

-- TimedIO tests

timeoutProp
    :: (MonadTimed m, MonadIO m, MonadCatch m)
    => NonNegative Microsecond
    -> NonNegative Microsecond
    -> PropertyM m ()
timeoutProp (getNonNegative -> tout) (getNonNegative -> wt) = do
    let action = do
            wait $ for wt
            return $ wt <= tout
        handler (_ :: MonadTimedError) = return $ wt >= tout
    res <- run $ timeout tout action `catch` handler
    assert res

invokeSemanticProp
    :: (MonadTimed m, MonadIO m)
    => RelativeToNowNat
    -> A
    -> Fun A A
    -> PropertyM m ()
invokeSemanticProp = actionTimeSemanticProp invoke

scheduleSemanticProp
    :: (MonadTimed m, MonadIO m)
    => RelativeToNowNat
    -> A
    -> Fun A A
    -> PropertyM m ()
scheduleSemanticProp = actionTimeSemanticProp schedule

actionTimeSemanticProp
    :: (MonadTimed m, MonadIO m)
    => (RelativeToNow -> m () -> m ())
    -> RelativeToNowNat
    -> A
    -> Fun A A
    -> PropertyM m ()
actionTimeSemanticProp action relativeToNow val f = do
    actionSemanticProp action' val f
    timePassingProp relativeToNow action'
  where
    action' = action $ fromIntegralRTN relativeToNow

forkSemanticProp
    :: (MonadTimed m, MonadIO m)
    => A
    -> Fun A A
    -> PropertyM m ()
forkSemanticProp = actionSemanticProp fork_

waitPassingProp
    :: (MonadTimed m, MonadIO m)
    => RelativeToNowNat
    -> PropertyM m ()
waitPassingProp relativeToNow =
    timePassingProp relativeToNow (wait (fromIntegralRTN relativeToNow) >>)

virtualTimePassingProp :: (MonadTimed m, MonadIO m) => PropertyM m ()
virtualTimePassingProp =
    timePassingProp 0 id

-- TODO: instead of testing with MVar's we should create PropertyM an instance of MonadTimed.
-- With that we could test inside forked/waited actions
-- | Tests that action will be exececuted after relativeToNow
timePassingProp
    :: (MonadTimed m, MonadIO m)
    => RelativeToNowNat
    -> (m () -> m ())
    -> PropertyM m ()
timePassingProp relativeToNow action = do
    mvar <- liftIO newEmptyMVar
    t1 <- run virtualTime
    run . action $ virtualTime >>= liftIO . putMVar mvar
    t2 <- liftIO $ takeMVar mvar
    monitor (counterexample $ mconcat
        [ "t1: ", show t1
        , ", t2: ", show t2, ", "
        , show $ fromIntegralRTN relativeToNow t1, " <= ", show t2
        ])
    assert $ fromIntegralRTN relativeToNow t1 <= t2

-- | Tests that an action will be executed
actionSemanticProp
    :: (MonadTimed m, MonadIO m)
    => (m () -> m ())
    -> A
    -> Fun A A
    -> PropertyM m ()
actionSemanticProp action val f = do
    mvar <- liftIO newEmptyMVar
    run . action . liftIO . putMVar mvar $ apply f val
    result <- liftIO $ takeMVar mvar
    monitor (counterexample $ mconcat
        [ "f: ", show f
        , ", val: ", show val
        , ", f val: ", show $ apply f val
        , ", should be: ", show result
        ])
    assert $ apply f val == result

-- TimedT tests
-- TODO: As TimedT is an instance of MonadIO, we can now reuse tests for TimedIO instead of these tests

-- TODO: use checkpoints timeout pattern from ExceptionSpec
killThreadTimedProp
    :: NonNegative Microsecond
    -> NonNegative Microsecond
    -> NonNegative Microsecond
    -> TimedTProp ()
killThreadTimedProp
    (getNonNegative -> mTime)
    (getNonNegative -> f1Time)
    (getNonNegative -> f2Time)
  = do
    var <- liftIO $ newTVarIO (0 :: Int)
    tId <- fork $ do
        fork_ $ do -- this thread can't be killed
            wait $ for f1Time
            liftIO $ atomically $ writeTVar var 1
        wait $ for f2Time
        liftIO $ atomically $ writeTVar var 2
    wait $ for mTime
    killThread tId
    wait $ for f1Time -- wait for both threads to finish
    wait $ for f2Time
    res <- liftIO $ readTVarIO var
    assertTimedT $ check res
  where
    check 0 = mTime <= f1Time && mTime <= f2Time
    check 1 = True -- this thread can't be killed
    check 2 = f2Time <= mTime
    check _ = error "This checkpoint doesn't exist"

timeoutTimedProp
    :: NonNegative Microsecond
    -> NonNegative Microsecond
    -> TimedTProp ()
timeoutTimedProp (getNonNegative -> tout) (getNonNegative -> wt) = do
    let action = do
            wait $ for wt
            return $ wt <= tout
        handler (_ :: SomeException) = do
            return $ tout <= wt
    res <- timeout tout action `catch` handler
    assertTimedT res

invokeSemanticTimedProp
    :: RelativeToNowNat
    -> A
    -> Fun A A
    -> TimedTProp ()
invokeSemanticTimedProp = actionTimeSemanticTimedProp invoke

scheduleSemanticTimedProp
    :: RelativeToNowNat
    -> A
    -> Fun A A
    -> TimedTProp ()
scheduleSemanticTimedProp = actionTimeSemanticTimedProp schedule

actionTimeSemanticTimedProp
    :: (RelativeToNow -> TimedTProp () -> TimedTProp ())
    -> RelativeToNowNat
    -> A
    -> Fun A A
    -> TimedTProp ()
actionTimeSemanticTimedProp action relativeToNow val f = do
    actionSemanticTimedProp action' val f
    timePassingTimedProp relativeToNow action'
  where
    action' = action $ fromIntegralRTN relativeToNow

forkSemanticTimedProp
    :: A
    -> Fun A A
    -> TimedTProp ()
forkSemanticTimedProp = actionSemanticTimedProp fork_

waitPassingTimedProp
    :: RelativeToNowNat
    -> TimedTProp ()
waitPassingTimedProp relativeToNow =
    timePassingTimedProp relativeToNow (wait (fromIntegralRTN relativeToNow) >>)

virtualTimePassingTimedProp :: TimedTProp ()
virtualTimePassingTimedProp =
    timePassingTimedProp 0 id

-- | Tests that action will be exececuted after relativeToNow
timePassingTimedProp
    :: RelativeToNowNat
    -> (TimedTProp () -> TimedTProp ())
    -> TimedTProp ()
timePassingTimedProp relativeToNow action = do
    t1 <- virtualTime
    action $ virtualTime >>= assertTimedT . (fromIntegralRTN relativeToNow t1 <=)

-- | Tests that an action will be executed
actionSemanticTimedProp
    :: (TimedTProp () -> TimedTProp ())
    -> A
    -> Fun A A
    -> TimedTProp ()
actionSemanticTimedProp action val f = do
    let result = apply f val
    action $ assertTimedT $ apply f val == result

nowProp :: Microsecond -> TimedTProp ()
nowProp ms = do
    wait $ for ms
    t1 <- virtualTime
    invoke now $ return ()
    t2 <- virtualTime
    assertTimedT $ t1 == t2


-- * Exceptions

data TestException = TestExc
    deriving (Show, Typeable)

instance Exception TestException

handleAll' :: MonadCatch m => m () -> m ()
handleAll' = handleAll $ const $ return ()


exceptionsThrown :: TimedTProp ()
exceptionsThrown = handleAll' $ do
    void $ throwM TestExc
    put False

exceptionsThrowCaught :: TimedTProp ()
exceptionsThrowCaught =
    let act = do
            put False
            throwM TestExc
        hnd = const $ put True
    in  act `catchAll` hnd

exceptionsWaitThrowCaught :: TimedTProp ()
exceptionsWaitThrowCaught =
    let act = do
            put False
            wait $ for 1 sec
            throwM TestExc
        hnd = const $ put True
    in  act `catchAll` hnd

exceptionNotAffectMainThread :: TimedTProp ()
exceptionNotAffectMainThread = handleAll' $ do
    put False
    fork_ $ throwM TestExc
    wait $ for 1 sec
    put True

exceptionNotAffectOtherThread :: TimedTProp ()
exceptionNotAffectOtherThread = handleAll' $ do
    put False
    schedule (after 3 sec) $ put True
    schedule (after 1 sec) $ throwM TestExc