mellon-core-0.7.0.0: test/Mellon/Controller/AsyncSpec.hs
{-# LANGUAGE DeriveDataTypeable #-}
module Mellon.Controller.AsyncSpec (spec) where
import Control.Concurrent (MVar, newMVar, modifyMVar, threadDelay)
import Control.Concurrent.Async (race)
import Control.Exception (Exception(..), throwIO)
import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.RWS.Strict (RWST, execRWST, ask, tell)
import Data.Data
import Data.Time (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime)
import qualified Data.Time as Time (getCurrentTime)
import Test.Hspec
import Mellon.Controller.Async
(Controller, State(..), controller, minUnlockTime, lockController,
queryController, unlockController)
import Mellon.Device
(Device(..), MockLock, MockLockEvent(..), events, mockLock,
mockLockDevice)
sleep :: (MonadIO m) => Int -> m ()
sleep = liftIO . threadDelay . (* 1000000)
getCurrentTime :: MonadIO m => m UTCTime
getCurrentTime = liftIO Time.getCurrentTime
timePlusN :: UTCTime -> Integer -> UTCTime
timePlusN time n = (fromInteger n) `addUTCTime` time
type TestController d a = RWST (Controller d) [MockLockEvent] () IO a
testController :: Controller d -> IO [MockLockEvent]
testController cc =
do (_, expectedResults) <- execRWST theTest cc ()
return expectedResults
where theTest :: TestController d ()
theTest =
do unlockWillExpire 5
sleep 8
unlockWontExpire 3
sleep 1
unlockExtend 10
sleep 14
unlockWillExpire 8
sleep 2
unlockWillBeIgnored 1
sleep 13
unlockWontExpire 8
sleep 3
lockIt
sleep 12
lockIt :: TestController d ()
lockIt =
do now <- getCurrentTime
cc <- ask
void $ lockController cc
tell [LockEvent now]
unlockIt :: Integer -> TestController d (UTCTime, UTCTime)
unlockIt duration =
do now <- getCurrentTime
cc <- ask
let expire = timePlusN now duration
void $ unlockController expire cc
return (now, expire)
unlockWillExpire :: Integer -> TestController d ()
unlockWillExpire duration =
do (now, expire) <- unlockIt duration
tell [UnlockEvent now]
tell [LockEvent expire]
unlockWontExpire :: Integer -> TestController d ()
unlockWontExpire duration =
do (now, _) <- unlockIt duration
tell [UnlockEvent now]
unlockExtend :: Integer -> TestController d ()
unlockExtend duration =
do (_, expire) <- unlockIt duration
tell [LockEvent expire]
unlockWillBeIgnored :: Integer -> TestController d ()
unlockWillBeIgnored duration =
do _ <- unlockIt duration
return ()
type CheckedResults = Either ((MockLockEvent, MockLockEvent), String) String
checkResults :: [MockLockEvent]
-> [MockLockEvent]
-> NominalDiffTime
-> CheckedResults
checkResults expected actual epsilon = foldr compareResult (Right "No results to compare") $ zip expected actual
where compareResult :: (MockLockEvent, MockLockEvent) -> CheckedResults -> CheckedResults
compareResult _ (Left l) = Left l
compareResult ev@(UnlockEvent t1, UnlockEvent t2) _ =
if t2 `diffUTCTime` t1 < epsilon
then Right "OK"
else Left (ev, "Time difference exceeds epsilon")
compareResult ev@(LockEvent t1, LockEvent t2) _ =
if t2 `diffUTCTime` t1 < epsilon
then Right "OK"
else Left (ev, "Time difference exceeds epsilon")
compareResult ev _ = Left (ev, "Event types don't match")
controllerTest :: IO CheckedResults
controllerTest =
do ml <- mockLock
cc <- controller (Just 1) $ mockLockDevice ml
ccEvents <- testController cc
-- Discard the first MockLock event, which happened when
-- controller initialized the lock.
_:lockEvents <- events ml
return $ checkResults ccEvents lockEvents (0.5 :: NominalDiffTime)
data ExceptionLock =
ExceptionLock {_ops :: !(MVar Int)
,_opsPerException :: !Int}
data ExceptionLockException =
LockException
deriving (Show,Typeable)
instance Exception ExceptionLockException
exceptionLock :: Int -> IO ExceptionLock
exceptionLock n =
do mvar <- newMVar 0
return $ ExceptionLock mvar n
-- | This device throws an exception every N operations.
exceptionLockDevice :: ExceptionLock -> Device ExceptionLock
exceptionLockDevice l =
Device inc
inc
where
inc =
do ops <- modifyMVar (_ops l) $ \n -> return (succ n, succ n)
when (ops `mod` (_opsPerException l) == 0) $
throwIO LockException
isExceptionLockException :: ExceptionLockException -> Bool
isExceptionLockException = const True
asyncExceptionTest :: IO ()
asyncExceptionTest =
do el <- exceptionLock 3
cc <- controller Nothing $ exceptionLockDevice el -- 1st lock op
now <- getCurrentTime
let expire = timePlusN now 3
unlockController expire cc -- 2nd & 3rd lock op (unlock, timed lock)
(sleep 5) `shouldThrow` isExceptionLockException -- async exception
queryController cc `shouldReturn` StateUnlocked expire -- should have state prior to exception
syncExceptionTest :: IO ()
syncExceptionTest =
do el <- exceptionLock 2
cc <- controller Nothing $ exceptionLockDevice el -- 1st lock op
now <- getCurrentTime
let expire = timePlusN now 3
unlockController expire cc `shouldThrow` isExceptionLockException -- 2nd lock op
queryController cc `shouldReturn` StateLocked -- should have state prior to exception
pastUnlockTimeTest :: IO ()
pastUnlockTimeTest =
do ml <- mockLock
cc <- controller Nothing $ mockLockDevice ml
race
(sleep 3) -- 3 sec should be more than enough time
(do now <- getCurrentTime
let past = timePlusN now (-1)
unlockController past cc)
`shouldReturn` Right StateLocked
ignoreUnlockTimeTest :: IO ()
ignoreUnlockTimeTest =
do ml <- mockLock
cc <- controller (Just 3) $ mockLockDevice ml
race
(sleep 3) -- 3 sec should be more than enough time
(do now <- getCurrentTime
let expire = timePlusN now 2
unlockController expire cc)
`shouldReturn` Right StateLocked
minUnlockTimeTest :: IO ()
minUnlockTimeTest =
do ml <- mockLock
cc1 <- controller Nothing $ mockLockDevice ml
minUnlockTime cc1 `shouldBe` 0
cc2 <- controller (Just (-1)) $ mockLockDevice ml
minUnlockTime cc2 `shouldBe` 0
spec :: Spec
spec = do
describe "Controller tests" $ do
it "should produce the correct lock sequence plus or minus a few hundred milliseconds" $ do
controllerTest >>= (`shouldBe` Right "OK")
it "should recover from asynchronous exceptions" $ do
asyncExceptionTest
it "should recover from synchronous exceptions" $ do
syncExceptionTest
it "should not wait forever if the unlock time is in the past" $ do
pastUnlockTimeTest
it "should ignore an unlock if its duration is less than the minimum unlock time" $ do
ignoreUnlockTimeTest
it "should set the minimum unlock time to 0 if the value is Nothing or < 0" $ do
minUnlockTimeTest