mirror of
https://github.com/barrucadu/dejafu.git
synced 2024-12-20 20:11:51 +03:00
8944ea97a5
This allows results to be naturally reported as lazy trees, rather than as lists representing a tree traversal. This in turn means that the actual bound can be moved outwards to the testing code, and not used at all in the runner. Trees let us do nice things with shrinking and short-circuiting, if we make the (fairly reasonable) assumption that the children of a buggy result will exhibit the same bug. Storing results as trees does complicate the predicate helper functions somewhat, but I think the clarity gained in the actual SCT code is well worth it.
334 lines
13 KiB
Haskell
334 lines
13 KiB
Haskell
{-# LANGUAGE Rank2Types #-}
|
|
|
|
-- | Deterministic testing for concurrent computations.
|
|
--
|
|
-- As an example, consider this program, which has two locks and a
|
|
-- shared variable. Two threads are spawned, which claim the locks,
|
|
-- update the shared variable, and release the locks. The main thread
|
|
-- waits for them both to terminate, and returns the final result.
|
|
--
|
|
-- > bad :: MonadConc m => m Int
|
|
-- > bad = do
|
|
-- > a <- newEmptyCVar
|
|
-- > b <- newEmptyCVar
|
|
-- >
|
|
-- > c <- newCVar 0
|
|
-- >
|
|
-- > j1 <- spawn $ lock a >> lock b >> modifyCVar_ c (return . succ) >> unlock b >> unlock a
|
|
-- > j2 <- spawn $ lock b >> lock a >> modifyCVar_ c (return . pred) >> unlock a >> unlock b
|
|
-- >
|
|
-- > takeCVar j1
|
|
-- > takeCVar j2
|
|
-- >
|
|
-- > takeCVar c
|
|
--
|
|
-- The correct result is 0, as it starts out as 0 and is incremented
|
|
-- and decremented by threads 1 and 2, respectively. However, note the
|
|
-- order of acquisition of the locks in the two threads. If thread 2
|
|
-- pre-empts thread 1 between the acquisition of the locks (or if
|
|
-- thread 1 pre-empts thread 2), a deadlock situation will arise, as
|
|
-- thread 1 will have lock @a@ and be waiting on @b@, and thread 2
|
|
-- will have @b@ and be waiting on @a@.
|
|
--
|
|
-- Here is what @dejafu@ has to say about it:
|
|
--
|
|
-- > > autocheck bad
|
|
-- > [fail] Never Deadlocks (checked: 4)
|
|
-- > [deadlock] S0---------S1-P2--S1-
|
|
-- > [deadlock] S0---------S2-P1--S2-
|
|
-- > [pass] No Exceptions (checked: 89)
|
|
-- > [fail] Consistent Result (checked: 3)
|
|
-- > [deadlock] S0---------S1-P2--S1-
|
|
-- > 0 S0---------S1--------S2--------S0-----
|
|
-- > [deadlock] S0---------S2-P1--S2-
|
|
-- > False
|
|
--
|
|
-- It identifies the deadlock, and also the possible results the
|
|
-- computation can produce, and displays a simplified trace leading to
|
|
-- each failing outcome. It also returns @False@ as there are test
|
|
-- failures. The automatic testing functionality is good enough if you
|
|
-- only want to check your computation is deterministic, but if you
|
|
-- have more specific requirements (or have some expected and
|
|
-- tolerated level of nondeterminism), you can write tests yourself
|
|
-- using the @dejafu*@ functions.
|
|
--
|
|
-- __Warning:__ If your computation under test does @IO@, the @IO@
|
|
-- will be executed lots of times! Be sure that it is deterministic
|
|
-- enough not to invalidate your test results.
|
|
module Test.DejaFu
|
|
( autocheck
|
|
, dejafu
|
|
, dejafus
|
|
, dejafus'
|
|
, autocheckIO
|
|
, dejafuIO
|
|
, dejafusIO
|
|
, dejafusIO'
|
|
-- * Test cases
|
|
, Result(..)
|
|
, Failure(..)
|
|
, runTest
|
|
, runTest'
|
|
, runTestIO
|
|
, runTestIO'
|
|
-- * Predicates
|
|
, Predicate
|
|
, deadlocksNever
|
|
, deadlocksAlways
|
|
, deadlocksSometimes
|
|
, exceptionsNever
|
|
, exceptionsAlways
|
|
, exceptionsSometimes
|
|
, alwaysSame
|
|
, notAlwaysSame
|
|
, alwaysTrue
|
|
, alwaysTrue2
|
|
, somewhereTrue
|
|
) where
|
|
|
|
import Control.Applicative ((<$>))
|
|
import Control.Arrow (first)
|
|
import Control.DeepSeq (NFData(..))
|
|
import Control.Monad (when)
|
|
import Data.List.Extra
|
|
import Test.DejaFu.Deterministic
|
|
import Test.DejaFu.Deterministic.IO (ConcIO)
|
|
import Test.DejaFu.SCT
|
|
|
|
-- | Run a test and print the result to stdout, return 'True' if it
|
|
-- passes.
|
|
dejafu :: (Eq a, Show a)
|
|
=> (forall t. Conc t a)
|
|
-- ^ The computation to test
|
|
-> (String, Predicate a)
|
|
-- ^ The test case, as a (name, predicate) pair.
|
|
-> IO Bool
|
|
dejafu conc test = dejafus conc [test]
|
|
|
|
-- | Variant of 'dejafu' for computations which do 'IO'.
|
|
dejafuIO :: (Eq a, Show a) => (forall t. ConcIO t a) -> (String, Predicate a) -> IO Bool
|
|
dejafuIO concio test = dejafusIO concio [test]
|
|
|
|
-- | Run a collection of tests, returning 'True' if all pass.
|
|
dejafus :: (Eq a, Show a) => (forall t. Conc t a) -> [(String, Predicate a)] -> IO Bool
|
|
dejafus = dejafus' 2
|
|
|
|
-- | Variant of 'dejafus' which takes a pre-emption bound.
|
|
dejafus' :: (Eq a, Show a) => Int -> (forall t. Conc t a) -> [(String, Predicate a)] -> IO Bool
|
|
dejafus' pb conc tests = do
|
|
let traces = sctPreBound conc
|
|
results <- mapM (\(name, test) -> doTest name $ runTest'' pb test traces) tests
|
|
return $ and results
|
|
|
|
-- | Variant of 'dejafus' for computations which do 'IO'.
|
|
dejafusIO :: (Eq a, Show a) => (forall t. ConcIO t a) -> [(String, Predicate a)] -> IO Bool
|
|
dejafusIO = dejafusIO' 2
|
|
|
|
-- | Variant of 'dejafus'' for computations which do 'IO'.
|
|
dejafusIO' :: (Eq a, Show a) => Int -> (forall t. ConcIO t a) -> [(String, Predicate a)] -> IO Bool
|
|
dejafusIO' pb concio tests = do
|
|
traces <- sctPreBoundIO concio
|
|
traces' <- mapM (sequenceIOTree $ Just pb) traces
|
|
results <- mapM (\(name, test) -> doTest name $ runTest'' pb test traces') tests
|
|
return $ and results
|
|
|
|
-- | Automatically test a computation. In particular, look for
|
|
-- deadlocks, uncaught exceptions, and multiple return values.
|
|
autocheck :: (Eq a, Show a) => (forall t. Conc t a) -> IO Bool
|
|
autocheck conc = dejafus conc cases where
|
|
cases = [ ("Never Deadlocks", deadlocksNever)
|
|
, ("No Exceptions", exceptionsNever)
|
|
, ("Consistent Result", alwaysSame)
|
|
]
|
|
|
|
-- | Variant of 'autocheck' for computations which do 'IO'.
|
|
autocheckIO :: (Eq a, Show a) => (forall t. ConcIO t a) -> IO Bool
|
|
autocheckIO concio = dejafusIO concio cases where
|
|
cases = [ ("Never Deadlocks", deadlocksNever)
|
|
, ("No Exceptions", exceptionsNever)
|
|
, ("Consistent Result", alwaysSame)
|
|
]
|
|
|
|
-- * Test cases
|
|
|
|
-- | The results of a test, including the number of cases checked to
|
|
-- determine the final boolean outcome.
|
|
data Result a = Result
|
|
{ _pass :: Bool
|
|
-- ^ Whether the test passed or not.
|
|
, _casesChecked :: Int
|
|
-- ^ The number of cases checked.
|
|
, _failures :: [(Either Failure a, Trace)]
|
|
-- ^ The failed cases, if any.
|
|
} deriving (Show, Eq)
|
|
|
|
instance NFData a => NFData (Result a) where
|
|
rnf r = rnf (_pass r, _casesChecked r, _failures r)
|
|
|
|
instance Functor Result where
|
|
fmap f r = r { _failures = map (first $ fmap f) $ _failures r }
|
|
|
|
-- | Run a predicate over all executions with two or fewer
|
|
-- pre-emptions. A pre-emption is a context switch where the old
|
|
-- thread was still runnable.
|
|
--
|
|
-- In the resultant traces, a pre-emption is displayed as \"Px\",
|
|
-- where @x@ is the ID of the thread being switched to, whereas a
|
|
-- regular context switch is displayed as \"Sx\" (for \"start\").
|
|
runTest :: Eq a => Predicate a -> (forall t. Conc t a) -> Result a
|
|
runTest = runTest' 2
|
|
|
|
-- | Variant of 'runTest' for computations which do 'IO'.
|
|
runTestIO :: Eq a => Predicate a -> (forall t. ConcIO t a) -> IO (Result a)
|
|
runTestIO = runTestIO' 2
|
|
|
|
-- | Variant of 'runTest' which takes a pre-emption bound.
|
|
runTest' :: Eq a => Int -> Predicate a -> (forall t. Conc t a) -> Result a
|
|
runTest' pb predicate conc = runTest'' pb predicate $ sctPreBound conc
|
|
|
|
-- | Variant of 'runTest'' which takes a tree of results and a depth limit.
|
|
runTest'' :: Eq a => Int -> Predicate a -> [SCTTree a] -> Result a
|
|
runTest'' pb predicate results = predicate $ map (bound pb) results where
|
|
bound 0 (SCTTree a t _) = SCTTree a t []
|
|
bound n (SCTTree a t os) = SCTTree a t $ map (bound $ n - 1) os
|
|
|
|
-- | Variant of 'runTest'' for computations which do 'IO'.
|
|
runTestIO' :: Eq a => Int -> Predicate a -> (forall t. ConcIO t a) -> IO (Result a)
|
|
runTestIO' pb predicate conc = do
|
|
results <- sctPreBoundIO conc
|
|
runTest'' pb predicate <$> mapM (sequenceIOTree $ Just pb) results
|
|
|
|
-- * Predicates
|
|
|
|
-- | A @Predicate@ is a function which collapses a list of results
|
|
-- into a 'Result'.
|
|
type Predicate a = [SCTTree a] -> Result a
|
|
|
|
-- | Check that a computation never deadlocks.
|
|
deadlocksNever :: Predicate a
|
|
deadlocksNever = alwaysTrue (not . either (`elem` [Deadlock, STMDeadlock]) (const False))
|
|
|
|
-- | Check that a computation always deadlocks.
|
|
deadlocksAlways :: Predicate a
|
|
deadlocksAlways = alwaysTrue $ either (`elem` [Deadlock, STMDeadlock]) (const False)
|
|
|
|
-- | Check that a computation deadlocks at least once.
|
|
deadlocksSometimes :: Predicate a
|
|
deadlocksSometimes = somewhereTrue $ either (`elem` [Deadlock, STMDeadlock]) (const False)
|
|
|
|
-- | Check that a computation never fails with an uncaught exception.
|
|
exceptionsNever :: Predicate a
|
|
exceptionsNever = alwaysTrue (not . either (==UncaughtException) (const False))
|
|
|
|
-- | Check that a computation always fails with an uncaught exception.
|
|
exceptionsAlways :: Predicate a
|
|
exceptionsAlways = alwaysTrue $ either (==UncaughtException) (const False)
|
|
|
|
-- | Check that a computation fails with an uncaught exception at least once.
|
|
exceptionsSometimes :: Predicate a
|
|
exceptionsSometimes = somewhereTrue $ either (==UncaughtException) (const False)
|
|
|
|
-- | Check that the result of a computation is always the same. In
|
|
-- particular this means either: (a) it always deadlocks, or (b) the
|
|
-- result is always 'Just' @x@, for some fixed @x@.
|
|
alwaysSame :: Eq a => Predicate a
|
|
alwaysSame = alwaysTrue2 (==)
|
|
|
|
-- | Check that the result of a computation is not always the same.
|
|
notAlwaysSame :: Eq a => Predicate a
|
|
notAlwaysSame ts = go ts Result { _pass = False, _casesChecked = 0, _failures = [] } where
|
|
go (SCTTree a t offs:sibs) res = case (offs, sibs) of
|
|
(SCTTree o u _:_, SCTTree s v _:_) -> case (a /= o, a /= s) of
|
|
(True, True) -> incCC . incCC $ res { _pass = True }
|
|
(True, False) -> incCC . incCC $ res { _pass = True, _failures = (a, t) : (s, v) : _failures res }
|
|
(False, True) -> incCC . incCC $ res { _pass = True, _failures = (a, t) : (o, u) : _failures res }
|
|
(False, False) -> go sibs . incCC . incCC $ res { _failures = (a, t) : (s, v) : (o, u) : _failures res }
|
|
|
|
(SCTTree o u _:_, [])
|
|
| a /= o -> incCC $ res { _pass = True }
|
|
| otherwise -> go (offs++sibs) . incCC $ res { _failures = (a, t) : (o, u) : _failures res }
|
|
|
|
([], SCTTree s v _:_)
|
|
| a /= s -> incCC $ res { _pass = True }
|
|
| otherwise -> go (offs++sibs) . incCC $ res { _failures = (a, t) : (s, v) : _failures res }
|
|
|
|
([], []) -> incCC res
|
|
|
|
go [] res = res
|
|
|
|
-- | Check that the result of a unary boolean predicate is always
|
|
-- true.
|
|
alwaysTrue :: (Either Failure a -> Bool) -> Predicate a
|
|
alwaysTrue p ts = go ts Result { _pass = True, _casesChecked = 0, _failures = [] } where
|
|
go (SCTTree a t offs:sibs) res
|
|
| p a = go (offs++sibs) . incCC $ res
|
|
| otherwise = (go sibs res { _failures = (a, t) : _failures res }) { _pass = False, _casesChecked = 1+_casesChecked res }
|
|
go [] res = res
|
|
|
|
-- | Check that the result of a binary boolean predicate is true
|
|
-- between all pairs of results. Only properties which are transitive
|
|
-- and symmetric should be used here.
|
|
--
|
|
-- If the predicate fails, /both/ (result,trace) tuples will be added
|
|
-- to the failures list.
|
|
alwaysTrue2 :: (Either Failure a -> Either Failure a -> Bool) -> Predicate a
|
|
alwaysTrue2 p ts = go ts Result { _pass = True, _casesChecked = 0, _failures = [] } where
|
|
go (SCTTree a t offs:sibs) res = case (offs, sibs) of
|
|
(SCTTree o u _:_, SCTTree s v _:_) -> case (p a o, p a s) of
|
|
(True, True) -> go (offs++sibs) . incCC . incCC $ res
|
|
(True, False) -> (go (offs++sibs) $ res { _failures = (a, t) : (s, v) : _failures res }) { _pass = False, _casesChecked = 2+_casesChecked res }
|
|
(False, True) -> (go sibs $ res { _failures = (a, t) : (o, u) : _failures res }) { _pass = False, _casesChecked = 2+_casesChecked res }
|
|
(False, False) -> (go sibs $ res { _failures = (a, t) : (s, v) : (o, u) : _failures res }) { _pass = False, _casesChecked = 2+_casesChecked res }
|
|
|
|
(SCTTree o u _:_, [])
|
|
| p a o -> go offs . incCC $ res
|
|
| otherwise -> incCC res { _pass = False, _failures = (a, t) : (o, u) : _failures res }
|
|
|
|
([], SCTTree s v _:_)
|
|
| p a s -> go sibs . incCC $ res
|
|
| otherwise -> incCC res { _pass = False, _failures = (a, t) : (s, v) : _failures res }
|
|
|
|
([], []) -> incCC res
|
|
go [] res = res
|
|
|
|
-- | Check that the result of a unary boolean predicate is true at
|
|
-- least once.
|
|
somewhereTrue :: (Either Failure a -> Bool) -> Predicate a
|
|
somewhereTrue p ts = go ts Result { _pass = False, _casesChecked = 0, _failures = [] } where
|
|
go (SCTTree a t offs:sibs) res
|
|
| p a = incCC res { _pass = True }
|
|
| otherwise = go (offs++sibs) $ incCC res { _failures = (a, t) : _failures res }
|
|
go [] res = res
|
|
|
|
-- * Internal
|
|
|
|
-- | Run a test and print to stdout
|
|
doTest :: (Eq a, Show a) => String -> Result a -> IO Bool
|
|
doTest name result = do
|
|
if _pass result
|
|
then
|
|
-- Display a pass message.
|
|
putStrLn $ "\27[32m[pass]\27[0m " ++ name ++ " (checked: " ++ show (_casesChecked result) ++ ")"
|
|
else do
|
|
-- Display a failure message, and the first 5 (simplified) failed traces
|
|
putStrLn ("\27[31m[fail]\27[0m " ++ name ++ " (checked: " ++ show (_casesChecked result) ++ ")")
|
|
|
|
let failures = _failures result
|
|
mapM_ (\(r, t) -> putStrLn $ "\t" ++ either showfail show r ++ " " ++ showTrace t) $ take 5 failures
|
|
when (moreThan failures 5) $
|
|
putStrLn "\t..."
|
|
|
|
return $ _pass result
|
|
|
|
-- | Increment the cases
|
|
incCC :: Result a -> Result a
|
|
incCC r = r { _casesChecked = _casesChecked r + 1 }
|
|
|
|
-- | Pretty-print a failure
|
|
showfail :: Failure -> String
|
|
showfail Deadlock = "[deadlock]"
|
|
showfail STMDeadlock = "[stm-deadlock]"
|
|
showfail InternalError = "[internal-error]"
|
|
showfail FailureInNoTest = "[_concNoTest]"
|
|
showfail UncaughtException = "[exception]"
|