dejafu/Test/DejaFu.hs
Michael Walker 8944ea97a5 Use schedule bounding as the primary SCT approach.
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.
2015-06-19 16:50:51 +01:00

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]"