Implement an atomic-for-testing-purposes function.

This adds a new `MonadConc` primitive, `_concNoTest`, which is (for
all non-test implementations) the identity function. For test
implementations, it is understood as "this action is completely safe
under all schedules, so just execute it all at once and don't consider
any internal interleavings." It is not required to be deterministic,
merely to never fail.

Actions annotated with `_concNoTest` will show up as one step in the
trace, and new `Failure` and `ThreadAction` values have been added.
This commit is contained in:
Michael Walker 2015-02-06 16:11:20 +00:00
parent c20db31561
commit 7aceb6a6f9
5 changed files with 98 additions and 45 deletions

View File

@ -86,6 +86,27 @@ class Monad m => MonadConc m where
-- returning 'Nothing'. -- returning 'Nothing'.
tryTakeCVar :: CVar m a -> m (Maybe a) tryTakeCVar :: CVar m a -> m (Maybe a)
-- | Runs its argument, just as if the @_concNoTest@ weren't there.
--
-- > _concNoTest x = x
--
-- This function is purely for testing purposes, and indicates that
-- it's not worth considering more than one schedule here. This is
-- useful if you have some larger computation built up out of
-- subcomputations which you have already got tests for: you only
-- want to consider what's unique to the large component.
--
-- The test runner will report a failure if the argument fails.
--
-- Note that inappropriate use of @_concNoTest@ can actually
-- /suppress/ bugs! For this reason it is recommended to use it only
-- for things which don't make use of any state from a larger
-- scope. As a rule-of-thumb: if you can't define it as a top-level
-- function taking no @CVar@ arguments, you probably shouldn't
-- @_concNoTest@ it.
_concNoTest :: m a -> m a
_concNoTest = id
instance MonadConc IO where instance MonadConc IO where
type CVar IO = MVar type CVar IO = MVar

View File

@ -25,6 +25,9 @@ module Test.DejaFu.Deterministic
, takeCVar , takeCVar
, tryTakeCVar , tryTakeCVar
-- * Testing
, _concNoTest
-- * Execution traces -- * Execution traces
, Trace , Trace
, Decision(..) , Decision(..)
@ -61,14 +64,14 @@ instance C.MonadConc (Conc t) where
readCVar = readCVar readCVar = readCVar
takeCVar = takeCVar takeCVar = takeCVar
tryTakeCVar = tryTakeCVar tryTakeCVar = tryTakeCVar
_concNoTest = _concNoTest
fixed :: Fixed Conc (ST t) (STRef t) t fixed :: Fixed (ST t) (STRef t)
fixed = F fixed = F
{ newRef = newSTRef { newRef = newSTRef
, readRef = readSTRef , readRef = readSTRef
, writeRef = writeSTRef , writeRef = writeSTRef
, liftN = \ma -> C $ cont (\c -> ALift $ c <$> ma) , liftN = \ma -> cont (\c -> ALift $ c <$> ma)
, getCont = unC
} }
-- | The concurrent variable type used with the 'Conc' monad. One -- | The concurrent variable type used with the 'Conc' monad. One
@ -115,6 +118,11 @@ takeCVar cvar = C $ cont $ ATake $ unV cvar
tryTakeCVar :: CVar t a -> Conc t (Maybe a) tryTakeCVar :: CVar t a -> Conc t (Maybe a)
tryTakeCVar cvar = C $ cont $ ATryTake $ unV cvar tryTakeCVar cvar = C $ cont $ ATryTake $ unV cvar
-- | Run the argument in one step. If the argument fails, the whole
-- computation will fail.
_concNoTest :: Conc t a -> Conc t a
_concNoTest ma = C $ cont $ \c -> ANoTest (unC ma) c
-- | Run a concurrent computation with a given 'Scheduler' and initial -- | Run a concurrent computation with a given 'Scheduler' and initial
-- state, returning a failure reason on error. Also returned is the -- state, returning a failure reason on error. Also returned is the
-- final state of the scheduler, and an execution trace. -- final state of the scheduler, and an execution trace.
@ -128,4 +136,4 @@ tryTakeCVar cvar = C $ cont $ ATryTake $ unV cvar
-- making your head hurt, check out the \"How @runST@ works\" section -- making your head hurt, check out the \"How @runST@ works\" section
-- of <https://ocharles.org.uk/blog/guest-posts/2014-12-18-rank-n-types.html> -- of <https://ocharles.org.uk/blog/guest-posts/2014-12-18-rank-n-types.html>
runConc :: Scheduler s -> s -> (forall t. Conc t a) -> (Either Failure a, s, Trace) runConc :: Scheduler s -> s -> (forall t. Conc t a) -> (Either Failure a, s, Trace)
runConc sched s ma = runST $ runFixed fixed sched s ma runConc sched s ma = runST $ runFixed fixed sched s $ unC ma

View File

@ -29,6 +29,9 @@ module Test.DejaFu.Deterministic.IO
, takeCVar , takeCVar
, tryTakeCVar , tryTakeCVar
-- * Testing
, _concNoTest
-- * Execution traces -- * Execution traces
, Trace , Trace
, Decision(..) , Decision(..)
@ -65,14 +68,14 @@ instance C.MonadConc (ConcIO t) where
readCVar = readCVar readCVar = readCVar
takeCVar = takeCVar takeCVar = takeCVar
tryTakeCVar = tryTakeCVar tryTakeCVar = tryTakeCVar
_concNoTest = _concNoTest
fixed :: Fixed ConcIO IO IORef t fixed :: Fixed IO IORef
fixed = F fixed = F
{ newRef = newIORef { newRef = newIORef
, readRef = readIORef , readRef = readIORef
, writeRef = writeIORef , writeRef = writeIORef
, liftN = liftIO , liftN = unC . liftIO
, getCont = unC
} }
-- | The concurrent variable type used with the 'ConcIO' monad. These -- | The concurrent variable type used with the 'ConcIO' monad. These
@ -120,9 +123,13 @@ takeCVar cvar = C $ cont $ ATake $ unV cvar
tryTakeCVar :: CVar t a -> ConcIO t (Maybe a) tryTakeCVar :: CVar t a -> ConcIO t (Maybe a)
tryTakeCVar cvar = C $ cont $ ATryTake $ unV cvar tryTakeCVar cvar = C $ cont $ ATryTake $ unV cvar
-- | Run the argument in one step. If the argument fails, the whole
-- computation will fail.
_concNoTest :: ConcIO t a -> ConcIO t a
_concNoTest ma = C $ cont $ \c -> ANoTest (unC ma) c
-- | Run a concurrent computation with a given 'Scheduler' and initial -- | Run a concurrent computation with a given 'Scheduler' and initial
-- state, returning an failure reason on error. Also returned is the -- state, returning an failure reason on error. Also returned is the
-- final state of the scheduler, and an execution trace. -- final state of the scheduler, and an execution trace.
runConcIO :: Scheduler s -> s -> (forall t. ConcIO t a) -> IO (Either Failure a, s, Trace) runConcIO :: Scheduler s -> s -> (forall t. ConcIO t a) -> IO (Either Failure a, s, Trace)
-- Note: Don't eta-reduce, the forall t messes up type inference. runConcIO sched s ma = runFixed fixed sched s $ unC ma
runConcIO sched s ma = runFixed fixed sched s ma

View File

@ -24,18 +24,15 @@ type M n r a = Cont (Action n r) a
type R r a = r (CVarId, Maybe a, [Block]) type R r a = r (CVarId, Maybe a, [Block])
-- | Dict of methods for concrete implementations to override. -- | Dict of methods for concrete implementations to override.
data Fixed c n r t = F data Fixed n r = F
{ newRef :: forall a. a -> n (r a) { newRef :: forall a. a -> n (r a)
-- ^ Create a new reference -- ^ Create a new reference
, readRef :: forall a. r a -> n a , readRef :: forall a. r a -> n a
-- ^ Read a reference. -- ^ Read a reference.
, writeRef :: forall a. r a -> a -> n () , writeRef :: forall a. r a -> a -> n ()
-- ^ Overwrite the contents of a reference. -- ^ Overwrite the contents of a reference.
, liftN :: forall a. n a -> c t a , liftN :: forall a. n a -> M n r a
-- ^ Lift an action from the underlying monad -- ^ Lift an action from the underlying monad
, getCont :: forall a. c t a -> M n r a
-- ^ Unpack the continuation-based computation from its wrapping
-- type.
} }
-- * Running @Conc@ Computations -- * Running @Conc@ Computations
@ -51,6 +48,7 @@ data Action n r =
| forall a. AGet (R r a) (a -> Action n r) | forall a. AGet (R r a) (a -> Action n r)
| forall a. ATake (R r a) (a -> Action n r) | forall a. ATake (R r a) (a -> Action n r)
| forall a. ATryTake (R r a) (Maybe a -> Action n r) | forall a. ATryTake (R r a) (Maybe a -> Action n r)
| forall a. ANoTest (M n r a) (a -> Action n r)
| ANew (CVarId -> n (Action n r)) | ANew (CVarId -> n (Action n r))
| ALift (n (Action n r)) | ALift (n (Action n r))
| AStop | AStop
@ -127,6 +125,9 @@ data ThreadAction =
-- ^ Get blocked on a take. -- ^ Get blocked on a take.
| TryTake CVarId Bool [ThreadId] | TryTake CVarId Bool [ThreadId]
-- ^ Try to take from a 'CVar', possibly waking up some threads. -- ^ Try to take from a 'CVar', possibly waking up some threads.
| NoTest
-- ^ A computation annotated with '_concNoTest' was executed in a
-- single step.
| Lift | Lift
-- ^ Lift an action from the underlying monad. Note that the -- ^ Lift an action from the underlying monad. Note that the
-- penultimate action in a trace will always be a @Lift@, this is an -- penultimate action in a trace will always be a @Lift@, this is an
@ -155,6 +156,10 @@ data Failure =
-- never arise unless you write your own, faulty, scheduler! If it -- never arise unless you write your own, faulty, scheduler! If it
-- does, please file a bug report. -- does, please file a bug report.
| Deadlock | Deadlock
-- ^ The computation became blocked indefinitely on @CVar@s
| FailureInNoTest
-- ^ A computation annotated with '_concNoTest' produced a failure,
-- rather than a result.
deriving (Eq, Show) deriving (Eq, Show)
instance NFData Failure where instance NFData Failure where
@ -164,12 +169,12 @@ instance NFData Failure where
-- state, returning a 'Just' if it terminates, and 'Nothing' if a -- state, returning a 'Just' if it terminates, and 'Nothing' if a
-- deadlock is detected. Also returned is the final state of the -- deadlock is detected. Also returned is the final state of the
-- scheduler, and an execution trace. -- scheduler, and an execution trace.
runFixed :: (Monad (c t), Monad n) => Fixed c n r t runFixed :: Monad n => Fixed n r
-> Scheduler s -> s -> c t a -> n (Either Failure a, s, Trace) -> Scheduler s -> s -> M n r a -> n (Either Failure a, s, Trace)
runFixed fixed sched s ma = do runFixed fixed sched s ma = do
ref <- newRef fixed Nothing ref <- newRef fixed Nothing
let c = getCont fixed $ ma >>= liftN fixed . writeRef fixed ref . Just . Right let c = ma >>= liftN fixed . writeRef fixed ref . Just . Right
let threads = M.fromList [(0, (runCont c $ const AStop, False))] let threads = M.fromList [(0, (runCont c $ const AStop, False))]
(s', trace) <- runThreads fixed (-1, 0) [] (negate 1) sched s threads ref (s', trace) <- runThreads fixed (-1, 0) [] (negate 1) sched s threads ref
@ -194,7 +199,7 @@ type Threads n r = Map ThreadId (Action n r, Bool)
-- efficient to prepend to a list than append. As this function isn't -- efficient to prepend to a list than append. As this function isn't
-- exposed to users of the library, this is just an internal gotcha to -- exposed to users of the library, this is just an internal gotcha to
-- watch out for. -- watch out for.
runThreads :: (Monad (c t), Monad n) => Fixed c n r t runThreads :: Monad n => Fixed n r
-> (CVarId, ThreadId) -> Trace -> ThreadId -> Scheduler s -> s -> Threads n r -> r (Maybe (Either Failure a)) -> n (s, Trace) -> (CVarId, ThreadId) -> Trace -> ThreadId -> Scheduler s -> s -> Threads n r -> r (Maybe (Either Failure a)) -> n (s, Trace)
runThreads fixed (lastcvid, lasttid) sofar prior sched s threads ref runThreads fixed (lastcvid, lasttid) sofar prior sched s threads ref
| isTerminated = return (s, sofar) | isTerminated = return (s, sofar)
@ -202,7 +207,9 @@ runThreads fixed (lastcvid, lasttid) sofar prior sched s threads ref
| isNonexistant = writeRef fixed ref (Just $ Left InternalError) >> return (s, sofar) | isNonexistant = writeRef fixed ref (Just $ Left InternalError) >> return (s, sofar)
| isBlocked = writeRef fixed ref (Just $ Left InternalError) >> return (s, sofar) | isBlocked = writeRef fixed ref (Just $ Left InternalError) >> return (s, sofar)
| otherwise = do | otherwise = do
(threads', act) <- stepThread (fst $ fromJust thread) fixed (lastcvid, lasttid) chosen threads stepped <- stepThread (fst $ fromJust thread) fixed (sched, s) (lastcvid, lasttid) chosen threads
case stepped of
Right (threads', act) -> do
let sofar' = (decision, alternatives, act) : sofar let sofar' = (decision, alternatives, act) : sofar
let lastcvid' = case act of { New c -> c; _ -> lastcvid } let lastcvid' = case act of { New c -> c; _ -> lastcvid }
@ -210,6 +217,8 @@ runThreads fixed (lastcvid, lasttid) sofar prior sched s threads ref
runThreads fixed (lastcvid', lasttid') sofar' chosen sched s' threads' ref runThreads fixed (lastcvid', lasttid') sofar' chosen sched s' threads' ref
Left failure -> writeRef fixed ref (Just $ Left failure) >> return (s, sofar)
where where
(chosen, s') = if prior == -1 then (0, s) else sched s prior $ head runnable' :| tail runnable' (chosen, s') = if prior == -1 then (0, s) else sched s prior $ head runnable' :| tail runnable'
runnable' = M.keys runnable runnable' = M.keys runnable
@ -232,10 +241,10 @@ runThreads fixed (lastcvid, lasttid) sofar prior sched s threads ref
-- | Run a single thread one step, by dispatching on the type of -- | Run a single thread one step, by dispatching on the type of
-- 'Action'. -- 'Action'.
stepThread :: (Monad (c t), Monad n) stepThread :: Monad n
=> Action n r => Action n r
-> Fixed c n r t -> (CVarId, ThreadId) -> ThreadId -> Threads n r -> n (Threads n r, ThreadAction) -> Fixed n r -> (Scheduler s, s) -> (CVarId, ThreadId) -> ThreadId -> Threads n r -> n (Either Failure (Threads n r, ThreadAction))
stepThread action fixed (lastcvid, lasttid) tid threads = case action of stepThread action fixed (scheduler, schedstate) (lastcvid, lasttid) tid threads = case action of
AFork a b -> stepFork a b AFork a b -> stepFork a b
APut ref a c -> stepPut ref a c APut ref a c -> stepPut ref a c
ATryPut ref a c -> stepTryPut ref a c ATryPut ref a c -> stepTryPut ref a c
@ -244,11 +253,12 @@ stepThread action fixed (lastcvid, lasttid) tid threads = case action of
ATryTake ref c -> stepTryTake ref c ATryTake ref c -> stepTryTake ref c
ANew na -> stepNew na ANew na -> stepNew na
ALift na -> stepLift na ALift na -> stepLift na
ANoTest ma a -> stepNoTest ma a
AStop -> stepStop AStop -> stepStop
where where
-- | Start a new thread, assigning it the next 'ThreadId' -- | Start a new thread, assigning it the next 'ThreadId'
stepFork a b = return (goto b tid threads', Fork newtid) where stepFork a b = return $ Right (goto b tid threads', Fork newtid) where
threads' = launch newtid a threads threads' = launch newtid a threads
newtid = lasttid + 1 newtid = lasttid + 1
@ -257,63 +267,71 @@ stepThread action fixed (lastcvid, lasttid) tid threads = case action of
stepPut ref a c = do stepPut ref a c = do
(success, threads', woken) <- putIntoCVar True ref a (const c) fixed tid threads (success, threads', woken) <- putIntoCVar True ref a (const c) fixed tid threads
cvid <- getCVarId fixed ref cvid <- getCVarId fixed ref
return (threads', if success then Put cvid woken else BlockedPut cvid) return $ Right (threads', if success then Put cvid woken else BlockedPut cvid)
-- | Try to put a value into a @CVar@, without blocking. -- | Try to put a value into a @CVar@, without blocking.
stepTryPut ref a c= do stepTryPut ref a c = do
(success, threads', woken) <- putIntoCVar False ref a c fixed tid threads (success, threads', woken) <- putIntoCVar False ref a c fixed tid threads
cvid <- getCVarId fixed ref cvid <- getCVarId fixed ref
return (threads', TryPut cvid success woken) return $ Right (threads', TryPut cvid success woken)
-- | Get the value from a @CVar@, without emptying, blocking the -- | Get the value from a @CVar@, without emptying, blocking the
-- thread until it's full. -- thread until it's full.
stepGet ref c = do stepGet ref c = do
(cvid, val, _) <- readRef fixed ref (cvid, val, _) <- readRef fixed ref
case val of case val of
Just val' -> return (goto (c val') tid threads, Read cvid) Just val' -> return $ Right (goto (c val') tid threads, Read cvid)
Nothing -> do Nothing -> do
threads' <- block fixed ref WaitFull tid threads threads' <- block fixed ref WaitFull tid threads
return (threads', BlockedRead cvid) return $ Right (threads', BlockedRead cvid)
-- | Take the value from a @CVar@, blocking the thread until it's -- | Take the value from a @CVar@, blocking the thread until it's
-- full. -- full.
stepTake ref c = do stepTake ref c = do
(success, threads', woken) <- takeFromCVar True ref (c . fromJust) fixed tid threads (success, threads', woken) <- takeFromCVar True ref (c . fromJust) fixed tid threads
cvid <- getCVarId fixed ref cvid <- getCVarId fixed ref
return (threads', if success then Take cvid woken else BlockedTake cvid) return $ Right (threads', if success then Take cvid woken else BlockedTake cvid)
-- | Try to take the value from a @CVar@, without blocking. -- | Try to take the value from a @CVar@, without blocking.
stepTryTake ref c = do stepTryTake ref c = do
(success, threads', woken) <- takeFromCVar True ref c fixed tid threads (success, threads', woken) <- takeFromCVar True ref c fixed tid threads
cvid <- getCVarId fixed ref cvid <- getCVarId fixed ref
return (threads', TryTake cvid success woken) return $ Right (threads', TryTake cvid success woken)
-- | Create a new @CVar@, using the next 'CVarId'. -- | Create a new @CVar@, using the next 'CVarId'.
stepNew na = do stepNew na = do
let newcvid = lastcvid + 1 let newcvid = lastcvid + 1
a <- na newcvid a <- na newcvid
return (goto a tid threads, New newcvid) return $ Right (goto a tid threads, New newcvid)
-- | Lift an action from the underlying monad into the @Conc@ -- | Lift an action from the underlying monad into the @Conc@
-- computation. -- computation.
stepLift na = do stepLift na = do
a <- na a <- na
return (goto a tid threads, Lift) return $ Right (goto a tid threads, Lift)
-- | Run a computation atomically. If this fails, the entire thing fails.
stepNoTest ma c = do
(a, _, _) <- runFixed fixed scheduler schedstate ma
return $
case a of
Right a' -> Right (goto (c a') tid threads, NoTest)
_ -> Left FailureInNoTest
-- | Kill the current thread. -- | Kill the current thread.
stepStop = return (kill tid threads, Stop) stepStop = return $ Right (kill tid threads, Stop)
-- * Manipulating @CVar@s -- * Manipulating @CVar@s
-- | Get the ID of a CVar -- | Get the ID of a CVar
getCVarId :: (Monad (c t), Monad n) => Fixed c n r t -> R r a -> n CVarId getCVarId :: Monad n => Fixed n r -> R r a -> n CVarId
getCVarId fixed ref = (\(cvid,_,_) -> cvid) `liftM` readRef fixed ref getCVarId fixed ref = (\(cvid,_,_) -> cvid) `liftM` readRef fixed ref
-- | Put a value into a @CVar@, in either a blocking or nonblocking -- | Put a value into a @CVar@, in either a blocking or nonblocking
-- way. -- way.
putIntoCVar :: (Monad (c t), Monad n) putIntoCVar :: Monad n
=> Bool -> R r a -> a -> (Bool -> Action n r) => Bool -> R r a -> a -> (Bool -> Action n r)
-> Fixed c n r t -> ThreadId -> Threads n r -> n (Bool, Threads n r, [ThreadId]) -> Fixed n r -> ThreadId -> Threads n r -> n (Bool, Threads n r, [ThreadId])
putIntoCVar blocking ref a c fixed threadid threads = do putIntoCVar blocking ref a c fixed threadid threads = do
(cvid, val, blocks) <- readRef fixed ref (cvid, val, blocks) <- readRef fixed ref
@ -333,9 +351,9 @@ putIntoCVar blocking ref a c fixed threadid threads = do
-- | Take a value from a @CVar@, in either a blocking or nonblocking -- | Take a value from a @CVar@, in either a blocking or nonblocking
-- way. -- way.
takeFromCVar :: (Monad (c t), Monad n) takeFromCVar :: Monad n
=> Bool -> R r a -> (Maybe a -> Action n r) => Bool -> R r a -> (Maybe a -> Action n r)
-> Fixed c n r t -> ThreadId -> Threads n r -> n (Bool, Threads n r, [ThreadId]) -> Fixed n r -> ThreadId -> Threads n r -> n (Bool, Threads n r, [ThreadId])
takeFromCVar blocking ref c fixed threadid threads = do takeFromCVar blocking ref c fixed threadid threads = do
(cvid, val, blocks) <- readRef fixed ref (cvid, val, blocks) <- readRef fixed ref
@ -360,7 +378,7 @@ goto :: Action n r -> ThreadId -> Threads n r -> Threads n r
goto a = M.alter $ \(Just (_, b)) -> Just (a, b) goto a = M.alter $ \(Just (_, b)) -> Just (a, b)
-- | Block a thread on a @CVar@. -- | Block a thread on a @CVar@.
block :: (Monad (c t), Monad n) => Fixed c n r t block :: Monad n => Fixed n r
-> R r a -> (ThreadId -> Block) -> ThreadId -> Threads n r -> n (Threads n r) -> R r a -> (ThreadId -> Block) -> ThreadId -> Threads n r -> n (Threads n r)
block fixed ref typ tid threads = do block fixed ref typ tid threads = do
(cvid, val, blocks) <- readRef fixed ref (cvid, val, blocks) <- readRef fixed ref
@ -376,7 +394,7 @@ kill :: ThreadId -> Threads n r -> Threads n r
kill = M.delete kill = M.delete
-- | Wake every thread blocked on a @CVar@ read/write. -- | Wake every thread blocked on a @CVar@ read/write.
wake :: (Monad (c t), Monad n) => Fixed c n r t wake :: Monad n => Fixed n r
-> R r a -> (ThreadId -> Block) -> Threads n r -> n (Threads n r, [ThreadId]) -> R r a -> (ThreadId -> Block) -> Threads n r -> n (Threads n r, [ThreadId])
wake fixed ref typ m = do wake fixed ref typ m = do
(m', woken) <- mapAndUnzipM wake' (M.toList m) (m', woken) <- mapAndUnzipM wake' (M.toList m)

View File

@ -7,7 +7,6 @@ module Test.DejaFu.SCT.Bounding where
import Control.DeepSeq (NFData(..), force) import Control.DeepSeq (NFData(..), force)
import Data.List.Extra import Data.List.Extra
import Test.DejaFu.Deterministic import Test.DejaFu.Deterministic
import Test.DejaFu.Deterministic.Internal
import Test.DejaFu.Deterministic.IO (ConcIO) import Test.DejaFu.Deterministic.IO (ConcIO)
import Test.DejaFu.SCT.Internal import Test.DejaFu.SCT.Internal