dejafu/dejafu-tests/Cases/MultiThreaded.hs
2017-02-25 06:01:59 +00:00

267 lines
7.6 KiB
Haskell

{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
module Cases.MultiThreaded where
import Control.Monad (void)
import System.Random (mkStdGen)
import Test.DejaFu (Failure(..), Predicate, Way(..), defaultBounds, defaultMemType, gives, gives')
import Test.Framework (Test, testGroup)
import Test.Framework.Providers.HUnit (hUnitTestToTests)
import Test.HUnit (test)
import Test.HUnit.DejaFu (testDejafuWay)
import Control.Concurrent.Classy
import Control.Monad.STM.Class
import Test.DejaFu.Conc (Conc, ConcST, subconcurrency)
data T where
T :: Show a => String -> (forall t. ConcST t a) -> Predicate a -> T
tests :: [Test]
tests =
[ testGroup "Threading" . tg $
[ T "child thread ID" threadId1 $ gives' [True]
, T "parent thread ID" threadId2 $ gives' [True]
, T "no wait" threadNoWait $ gives' [Nothing, Just ()]
]
, testGroup "MVar" . tg $
[ T "deadlock" cvarLock $ gives [Left Deadlock, Right 0]
, T "race" cvarRace $ gives' [0,1]
]
, testGroup "CRef" . tg $
[ T "race" crefRace $ gives' [0,1]
]
, testGroup "STM" . tg $
[ T "atomicity" stmAtomic $ gives' [0,2]
, T "left retry" stmLeftRetry $ gives' [()]
, T "right retry" stmRightRetry $ gives' [()]
, T "issue 55" stmIssue55 $ gives' [True]
]
, testGroup "Killing Threads" . tg $
[ T "no masking" threadKill $ gives [Left Deadlock, Right ()]
, T "masked" threadKillMask $ gives' [()]
, T "unmasked" threadKillUmask $ gives [Left Deadlock, Right ()]
]
, testGroup "Daemons" . tg $
[ T "schedule daemon" schedDaemon $ gives' [0,1]
]
, testGroup "Subconcurrency" . tg $
[ T "deadlock1" scDeadlock1 $ gives' [Left Deadlock, Right ()]
, T "deadlock2" scDeadlock2 $ gives' [(Left Deadlock, ()), (Right (), ())]
, T "success" scSuccess $ gives' [Right ()]
, T "illegal" scIllegal $ gives [Left IllegalSubconcurrency]
, T "issue 71" scIssue71 $ gives' [()]
]
]
where
tg ts =
let useWay way = map (\(T n c p) -> testDejafuWay way defaultMemType c n p) ts
in [ testGroup "Systematic" . hUnitTestToTests . test . useWay $ Systematically defaultBounds
, testGroup "Random" . hUnitTestToTests . test . useWay $ Randomly (mkStdGen 0) 100
]
--------------------------------------------------------------------------------
-- Threading
-- | Fork reports the good @ThreadId@.
threadId1 :: MonadConc m => m Bool
threadId1 = do
var <- newEmptyMVar
tid <- fork $ myThreadId >>= putMVar var
(tid ==) <$> readMVar var
-- | A child and parent thread have different @ThreadId@s.
threadId2 :: MonadConc m => m Bool
threadId2 = do
tid <- spawn myThreadId
(/=) <$> myThreadId <*> readMVar tid
-- | A parent thread doesn't wait for child threads before
-- terminating.
threadNoWait :: MonadConc m => m (Maybe ())
threadNoWait = do
x <- newCRef Nothing
void . fork . writeCRef x $ Just ()
readCRef x
--------------------------------------------------------------------------------
-- @MVar@s
-- | Deadlocks sometimes due to order of acquision of locks.
cvarLock :: MonadConc m => m Int
cvarLock = do
a <- newEmptyMVar
b <- newEmptyMVar
c <- newMVar 0
let lock m = putMVar m ()
let unlock = takeMVar
j1 <- spawn $ lock a >> lock b >> modifyMVar_ c (return . succ) >> unlock b >> unlock a
j2 <- spawn $ lock b >> lock a >> modifyMVar_ c (return . pred) >> unlock a >> unlock b
takeMVar j1
takeMVar j2
takeMVar c
-- | When racing two @putMVar@s, one of them will win.
cvarRace :: MonadConc m => m Int
cvarRace = do
x <- newEmptyMVar
void . fork $ putMVar x 0
void . fork $ putMVar x 1
readMVar x
--------------------------------------------------------------------------------
-- @CRef@s
--
-- TODO: Tests on CAS operations
-- | When racing two @writeCRef@s, one of them will win.
crefRace :: MonadConc m => m Int
crefRace = do
x <- newCRef (0::Int)
j1 <- spawn $ writeCRef x 0
j2 <- spawn $ writeCRef x 1
takeMVar j1
takeMVar j2
readCRef x
--------------------------------------------------------------------------------
-- STM
-- | Transactions are atomic.
stmAtomic :: MonadConc m => m Int
stmAtomic = do
x <- atomically $ newTVar (0::Int)
void . fork . atomically $ writeTVar x 1 >> writeTVar x 2
atomically $ readTVar x
-- | 'retry' is the left identity of 'orElse'.
stmLeftRetry :: MonadConc m => m ()
stmLeftRetry = do
x <- atomically $ newTVar Nothing
let readJust var = maybe retry pure =<< readTVar var
fork . atomically . writeTVar x $ Just ()
atomically $ retry `orElse` readJust x
-- | 'retry' is the right identity of 'orElse'.
stmRightRetry :: MonadConc m => m ()
stmRightRetry = do
x <- atomically $ newTVar Nothing
let readJust var = maybe retry pure =<< readTVar var
fork . atomically . writeTVar x $ Just ()
atomically $ readJust x `orElse` retry
-- | Test case from issue #55.
stmIssue55 :: MonadConc m => m Bool
stmIssue55 = do
a <- atomically $ newTQueue
b <- atomically $ newTQueue
fork . atomically $ writeTQueue b True
let both a b = readTQueue a `orElse` readTQueue b `orElse` retry
atomically $ both a b
--------------------------------------------------------------------------------
-- Exceptions
-- | Cause a deadlock sometimes by killing a thread.
threadKill :: MonadConc m => m ()
threadKill = do
x <- newEmptyMVar
tid <- fork $ putMVar x ()
killThread tid
readMVar x
-- | Never deadlock by masking a thread.
threadKillMask :: MonadConc m => m ()
threadKillMask = do
x <- newEmptyMVar
y <- newEmptyMVar
tid <- fork $ mask $ \_ -> putMVar x () >> putMVar y ()
readMVar x
killThread tid
readMVar y
-- | Sometimes deadlock by killing a thread.
threadKillUmask :: MonadConc m => m ()
threadKillUmask = do
x <- newEmptyMVar
y <- newEmptyMVar
tid <- fork $ mask $ \umask -> putMVar x () >> umask (putMVar y ())
readMVar x
killThread tid
readMVar y
-------------------------------------------------------------------------------
-- Daemon threads
-- | Fork off a thread where the first action has no dependency with
-- anything the initial thread does, but which has a later action
-- which does. This exhibits issue #40.
schedDaemon :: MonadConc m => m Int
schedDaemon = do
x <- newCRef 0
_ <- fork $ myThreadId >> writeCRef x 1
readCRef x
--------------------------------------------------------------------------------
-- Subconcurrency
-- | Subcomputation deadlocks sometimes.
scDeadlock1 :: Monad n => Conc n r (Either Failure ())
scDeadlock1 = do
var <- newEmptyMVar
subconcurrency $ do
void . fork $ putMVar var ()
putMVar var ()
-- | Subcomputation deadlocks sometimes, and action after it still
-- happens.
scDeadlock2 :: Monad n => Conc n r (Either Failure (), ())
scDeadlock2 = do
var <- newEmptyMVar
res <- subconcurrency $ do
void . fork $ putMVar var ()
putMVar var ()
(,) <$> pure res <*> readMVar var
-- | Subcomputation successfully completes.
scSuccess :: Monad n => Conc n r (Either Failure ())
scSuccess = do
var <- newMVar ()
subconcurrency $ do
out <- newEmptyMVar
void . fork $ takeMVar var >>= putMVar out
takeMVar out
-- | Illegal usage
scIllegal :: Monad n => Conc n r ()
scIllegal = do
var <- newEmptyMVar
void . fork $ readMVar var
void . subconcurrency $ pure ()
-- | Test case from issue 71. This won't fail if the bug is
-- reintroduced, it will just hang.
scIssue71 :: Monad n => Conc n r ()
scIssue71 = do
let ma ||| mb = do { j1 <- spawn ma; j2 <- spawn mb; takeMVar j1; takeMVar j2; pure () }
s <- newEmptyMVar
_ <- subconcurrency (takeMVar s ||| pure ())
pure ()