rawlock-0.1.2.0: test/Main.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main (main) where
import Control.Concurrent.Class.MonadSTM.Strict
import Control.Exception (throw)
import Control.Monad.Class.MonadAsync
import Control.Monad.Class.MonadFork
import Control.Monad.Class.MonadSay
import Control.Monad.Class.MonadTest
import Control.Monad.Class.MonadThrow
import Control.Monad.Class.MonadTimer
import Control.Monad.Except
import Control.Monad.IOSim
import Control.RAWLock
import Data.Either
import Test.QuickCheck.Gen.Unsafe
import Test.QuickCheck.Monadic
import Test.Tasty
import Test.Tasty.QuickCheck
main :: IO ()
main =
defaultMain $
testGroup
"RAWLock"
[ testProperty "Exception safe" $ conjoin (map prop_exception_safe allCommandCombinations)
, testProperty "correctness" prop_correctness
, testProperty "unsafe functions do not deadlock" $
conjoin (map prop_unsafe_actions allCommandCombinations)
]
{-------------------------------------------------------------------------------
Exception safe
-------------------------------------------------------------------------------}
data Action = Read | Incr | Append
deriving Show
allCommandCombinations :: [(Action, Action, Action)]
allCommandCombinations =
[(a, b, c) | let cmds = [Read, Incr, Append], a <- cmds, b <- cmds, c <- cmds]
prop_exception_safe :: (Action, Action, Action) -> Property
prop_exception_safe actions@(a1, a2, a3) =
counterexample (show actions) $
exploreSimTrace
id
action
(\_ tr -> counterexample (ppTrace tr) . property . isRight . traceResult False $ tr)
where
action :: IOSim s ()
action = do
exploreRaces
l <- new (0 :: Int)
let c = \case
Read -> withReadAccess l $ \s -> say (show s)
Incr -> withWriteAccess l $ \s -> say (show s) >> pure ((), s + 1)
Append -> withAppendAccess l $ \s -> say (show s) >> pure ((), s + 1)
t1 <- async $ c a1
t2 <- async $ c a2
t3 <- async $ c a3
async (cancel t1) >>= wait
(_ :: Either SomeException ()) <- waitCatch t1
(_ :: Either SomeException ()) <- waitCatch t2
(_ :: Either SomeException ()) <- waitCatch t3
pure ()
{-------------------------------------------------------------------------------
Correctness
-------------------------------------------------------------------------------}
-- | Test the correctness of the RAWLock
--
-- For a generated number of readers, appenders, and writers: spawn a thread
-- for each. Each thread will process a list of 'ThreadDelays'. For each
-- 'ThreadDelays': wait the generated 'beforeLockTime', then lock the RAWLock
-- with the respective @RAWLock.withXAccess@, increment a 'TVar' that stores
-- the number of readers/appenders/writers that have access, hold the lock for
-- a generated 'withLockTime', decrement the 'TVar', and release the lock.
--
-- In a separate thread, we watch for any changes in the three 'TVar's and
-- write each changed 'RAWState' to a trace (in a separate 'TVar').
-- Afterwards, we check the 'RAWState's in the trace for consistency (using
-- 'isConsistent'), e.g., not more than one concurrent appender.
prop_correctness :: TestSetup -> Property
prop_correctness (TestSetup rawDelays) =
monadicSimWithTrace tabulateBlockeds test
where
RAW readerDelays appenderDelays writerDelays = rawDelays
test :: forall s. PropertyM (IOSim s) ()
test = do
rawVars@(RAW varReaders varAppenders varWriters) <- run newRAWVars
trace <- run $ do
rawLock <- new ()
varTrace <- newTVarIO []
let traceState :: STM (IOSim s) ()
traceState = do
rawState <- readRAWState rawVars
modifyTVar varTrace (rawState :)
threads <-
mapM (async . (labelThisThread "testThread" >>)) $
map (runReader rawLock traceState varReaders) readerDelays
<> map (runAppender rawLock traceState varAppenders) appenderDelays
<> map (runWriter rawLock traceState varWriters) writerDelays
mapM_ wait threads
reverse <$> atomically (readTVar varTrace)
checkRAWTrace trace
runReader ::
RAWLock (IOSim s) () ->
STM (IOSim s) () ->
-- \^ Trace the 'RAWState'
StrictTVar (IOSim s) Int ->
[ThreadDelays] ->
IOSim s ()
runReader rawLock traceState varReaders =
mapM_ $ \(ThreadDelays before with) -> do
threadDelay before
withReadAccess rawLock $ const $ do
atomically $ modifyTVar varReaders succ *> traceState
threadDelay with
atomically $ modifyTVar varReaders pred *> traceState
runAppender ::
RAWLock (IOSim s) () ->
STM (IOSim s) () ->
-- \^ Trace the 'RAWState'
StrictTVar (IOSim s) Int ->
[ThreadDelays] ->
IOSim s ()
runAppender rawLock traceState varAppenders =
mapM_ $ \(ThreadDelays before with) -> do
threadDelay before
withAppendAccess rawLock $ const $ do
atomically $ modifyTVar varAppenders succ *> traceState
threadDelay with
atomically $ modifyTVar varAppenders pred *> traceState
return ((), ())
runWriter ::
RAWLock (IOSim s) () ->
STM (IOSim s) () ->
-- \^ Trace the 'RAWState'
StrictTVar (IOSim s) Int ->
[ThreadDelays] ->
IOSim s ()
runWriter rawLock traceState varWriters =
mapM_ $ \(ThreadDelays before with) -> do
threadDelay before
withWriteAccess rawLock $ const $ do
atomically $ modifyTVar varWriters succ *> traceState
threadDelay with
atomically $ modifyTVar varWriters pred *> traceState
return ((), ())
-- | Like 'monadicSim' (which is like 'monadicIO' for the IO simulator), but
-- allows inspecting the trace for labelling purposes.
monadicSimWithTrace ::
Testable a =>
(forall x. SimTrace x -> Property -> Property) ->
(forall s. PropertyM (IOSim s) a) ->
Property
monadicSimWithTrace attachTrace m = property $ do
tr <- runSimGenWithTrace (monadic' m)
case traceResult False tr of
Left failure -> throw failure
Right prop -> return $ attachTrace tr prop
where
runSimGenWithTrace :: (forall s. Gen (IOSim s a)) -> Gen (SimTrace a)
runSimGenWithTrace f = do
Capture eval <- capture
return $ runSimTrace (eval f)
-- | Tabulate the number of times a thread is blocked.
--
-- The higher this number, the higher the contention. If there's no
-- contention, we're not testing the lock properly.
tabulateBlockeds :: SimTrace a -> Property -> Property
tabulateBlockeds tr =
tabulate "number of times blocked" [classifyBand (count isBlocked tr)]
where
isBlocked (EventTxBlocked{}) = Just ()
isBlocked _ = Nothing
count :: (SimEventType -> Maybe x) -> SimTrace a -> Int
count p = length . selectTraceEvents (const p)
classifyBand :: Int -> String
classifyBand n
| n < 10 =
"n < 10"
| n < 100 =
"n < 100"
| n < 1000 =
"n < 1,000"
| n < 10_000 =
"1,000 < n < 10,000"
| n < 100_000 =
"10,000 < n < 100,000"
| n < 1000_000 =
"100,000 < n < 1,000,000"
| otherwise =
"1,000,000 < n"
{-------------------------------------------------------------------------------
State checking
-------------------------------------------------------------------------------}
-- | Data type reused whenever we need something for all three of them.
data RAW a = RAW
{ readers :: a
, appenders :: a
, writers :: a
}
deriving (Show, Eq, Functor)
type RAWVars m = RAW (StrictTVar m Int)
newRAWVars :: IOSim s (RAWVars (IOSim s))
newRAWVars = RAW <$> newTVarIO 0 <*> newTVarIO 0 <*> newTVarIO 0
type RAWState' = RAW Int
readRAWState :: RAWVars (IOSim s) -> STM (IOSim s) RAWState'
readRAWState RAW{readers, appenders, writers} =
RAW
<$> readTVar readers
<*> readTVar appenders
<*> readTVar writers
isConsistent :: RAWState' -> Except String ()
isConsistent RAW{readers, appenders, writers}
| appenders > 1 =
throwError $ show appenders <> " appenders while at most 1 is allowed"
| writers > 1 =
throwError $ show writers <> " writers while at most 1 is allowed"
| writers == 1
, readers > 0 =
throwError $ "writer concurrent with " <> show readers <> "reader(s)"
| writers == 1
, appenders > 0 =
throwError $ "writer concurrent with an appender"
| otherwise =
return ()
type RAWTrace = [RAWState']
checkRAWTrace :: Monad m => RAWTrace -> PropertyM m ()
checkRAWTrace = mapM_ $ \rawState ->
case runExcept $ isConsistent rawState of
Left msg -> do
monitor (counterexample msg)
Test.QuickCheck.Monadic.assert False
Right () ->
return ()
{-------------------------------------------------------------------------------
Generators
-------------------------------------------------------------------------------}
newtype TestSetup = TestSetup (RAW [[ThreadDelays]])
deriving Show
instance Arbitrary TestSetup where
arbitrary = do
nbReaders <- choose (0, 3)
nbAppenders <- choose (0, 3)
nbWriters <- choose (0, 3)
readers <- vectorOf nbReaders arbitrary
appenders <- vectorOf nbAppenders arbitrary
writers <- vectorOf nbWriters arbitrary
return $ TestSetup RAW{readers, appenders, writers}
shrink (TestSetup raw@RAW{readers, appenders, writers}) =
[TestSetup raw{readers = readers'} | readers' <- shrink readers]
<> [TestSetup raw{appenders = appenders'} | appenders' <- shrink appenders]
<> [TestSetup raw{writers = writers'} | writers' <- shrink writers]
data ThreadDelays = ThreadDelays
{ beforeLockTime :: Int
-- ^ How long the thread should wait before it starts to take the lock
, withLockTime :: Int
-- ^ How long the thread should wait while holding the lock
}
deriving (Eq, Show)
instance Arbitrary ThreadDelays where
arbitrary = do
beforeLockTime <- choose (0, 1000)
withLockTime <- choose (0, 2000)
return ThreadDelays{beforeLockTime, withLockTime}
{-------------------------------------------------------------------------------
unsafe functions
-------------------------------------------------------------------------------}
prop_unsafe_actions :: (Action, Action, Action) -> Property
prop_unsafe_actions actions@(a1, a2, a3) =
counterexample (show actions) $
exploreSimTrace
id
action
(\_ tr -> counterexample (ppTrace tr) . property . isRight . traceResult False $ tr)
where
action :: IOSim s ()
action = do
exploreRaces
l <- new (0 :: Int)
let c = \case
Read ->
bracket
(atomically (unsafeAcquireReadAccess l))
(const $ atomically (unsafeReleaseReadAccess l))
(say . ("Read: " <>) . show)
Incr ->
generalBracket
(unsafeAcquireWriteAccess l)
( \orig -> \case
ExitCaseSuccess s' -> unsafeReleaseWriteAccess l s'
_ -> unsafeReleaseWriteAccess l orig
)
( \s -> do
say ("Incr: " <> show s)
pure (s + 1)
)
>> pure ()
Append ->
generalBracket
(unsafeAcquireAppendAccess l)
( \orig -> \case
ExitCaseSuccess s' -> unsafeReleaseAppendAccess l s'
_ -> unsafeReleaseAppendAccess l orig
)
( \s -> do
say ("Append: " <> show s)
pure (s + 1)
)
>> pure ()
t1 <- async $ c a1
t2 <- async $ c a2
t3 <- async $ c a3
async (cancel t1) >>= wait
(_ :: Either SomeException ()) <- waitCatch t1
(_ :: Either SomeException ()) <- waitCatch t2
(_ :: Either SomeException ()) <- waitCatch t3
pure ()