2015-01-21 18:31:10 +03:00
|
|
|
{-# LANGUAGE Rank2Types #-}
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | 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-
|
2015-02-17 14:22:17 +03:00
|
|
|
-- > [pass] No Exceptions (checked: 89)
|
2015-02-01 04:21:42 +03:00
|
|
|
-- > [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.
|
2015-01-31 18:50:54 +03:00
|
|
|
module Test.DejaFu
|
2015-02-01 04:21:42 +03:00
|
|
|
( autocheck
|
|
|
|
, dejafu
|
|
|
|
, dejafus
|
2015-05-26 06:14:10 +03:00
|
|
|
, dejafus'
|
2015-01-30 19:09:32 +03:00
|
|
|
, autocheckIO
|
2015-02-01 04:21:42 +03:00
|
|
|
, dejafuIO
|
|
|
|
, dejafusIO
|
2015-05-26 06:14:10 +03:00
|
|
|
, dejafusIO'
|
2015-01-23 20:06:14 +03:00
|
|
|
-- * Test cases
|
|
|
|
, Result(..)
|
2015-02-06 18:24:47 +03:00
|
|
|
, Failure(..)
|
2015-01-12 20:27:41 +03:00
|
|
|
, runTest
|
2015-01-12 20:52:30 +03:00
|
|
|
, runTest'
|
2015-02-01 04:21:42 +03:00
|
|
|
, runTestIO
|
2015-01-12 20:52:30 +03:00
|
|
|
, runTestIO'
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Predicates
|
|
|
|
, Predicate
|
|
|
|
, deadlocksNever
|
|
|
|
, deadlocksAlways
|
2015-01-15 05:16:02 +03:00
|
|
|
, deadlocksSometimes
|
2015-02-17 08:58:25 +03:00
|
|
|
, exceptionsNever
|
|
|
|
, exceptionsAlways
|
|
|
|
, exceptionsSometimes
|
2015-01-12 20:27:41 +03:00
|
|
|
, alwaysSame
|
2015-02-01 04:21:42 +03:00
|
|
|
, notAlwaysSame
|
2015-01-12 22:08:09 +03:00
|
|
|
, alwaysTrue
|
|
|
|
, alwaysTrue2
|
2015-01-15 05:16:02 +03:00
|
|
|
, somewhereTrue
|
2015-01-12 20:27:41 +03:00
|
|
|
) where
|
|
|
|
|
2015-06-19 18:50:51 +03:00
|
|
|
import Control.Applicative ((<$>))
|
2015-01-23 19:48:38 +03:00
|
|
|
import Control.Arrow (first)
|
2015-01-19 14:50:43 +03:00
|
|
|
import Control.DeepSeq (NFData(..))
|
2015-02-01 04:21:42 +03:00
|
|
|
import Control.Monad (when)
|
2015-01-27 16:46:20 +03:00
|
|
|
import Data.List.Extra
|
2015-01-31 18:50:54 +03:00
|
|
|
import Test.DejaFu.Deterministic
|
2015-02-01 04:21:42 +03:00
|
|
|
import Test.DejaFu.Deterministic.IO (ConcIO)
|
|
|
|
import Test.DejaFu.SCT
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | 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
|
2015-05-26 06:14:10 +03:00
|
|
|
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
|
2015-07-17 00:32:30 +03:00
|
|
|
let traces = sctPreBound pb conc
|
|
|
|
results <- mapM (\(name, test) -> doTest name $ test traces) tests
|
2015-02-01 04:21:42 +03:00
|
|
|
return $ and results
|
2015-01-26 20:36:25 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Variant of 'dejafus' for computations which do 'IO'.
|
|
|
|
dejafusIO :: (Eq a, Show a) => (forall t. ConcIO t a) -> [(String, Predicate a)] -> IO Bool
|
2015-05-26 06:14:10 +03:00
|
|
|
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
|
2015-07-17 00:32:30 +03:00
|
|
|
traces <- sctPreBoundIO pb concio
|
|
|
|
results <- mapM (\(name, test) -> doTest name $ test traces) tests
|
2015-01-23 20:06:14 +03:00
|
|
|
return $ and results
|
|
|
|
|
2015-01-30 19:09:32 +03:00
|
|
|
-- | Automatically test a computation. In particular, look for
|
2015-02-17 14:22:17 +03:00
|
|
|
-- deadlocks, uncaught exceptions, and multiple return values.
|
2015-01-30 19:09:32 +03:00
|
|
|
autocheck :: (Eq a, Show a) => (forall t. Conc t a) -> IO Bool
|
2015-02-01 04:21:42 +03:00
|
|
|
autocheck conc = dejafus conc cases where
|
|
|
|
cases = [ ("Never Deadlocks", deadlocksNever)
|
2015-02-17 14:22:17 +03:00
|
|
|
, ("No Exceptions", exceptionsNever)
|
2015-02-01 04:21:42 +03:00
|
|
|
, ("Consistent Result", alwaysSame)
|
2015-01-30 19:09:32 +03:00
|
|
|
]
|
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | 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)
|
2015-02-17 14:22:17 +03:00
|
|
|
, ("No Exceptions", exceptionsNever)
|
2015-02-01 04:21:42 +03:00
|
|
|
, ("Consistent Result", alwaysSame)
|
|
|
|
]
|
2015-01-26 20:36:25 +03:00
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Test cases
|
|
|
|
|
2015-06-19 18:50:51 +03:00
|
|
|
-- | The results of a test, including the number of cases checked to
|
|
|
|
-- determine the final boolean outcome.
|
2015-01-23 19:48:38 +03:00
|
|
|
data Result a = Result
|
2015-01-12 20:27:41 +03:00
|
|
|
{ _pass :: Bool
|
|
|
|
-- ^ Whether the test passed or not.
|
|
|
|
, _casesChecked :: Int
|
|
|
|
-- ^ The number of cases checked.
|
2015-02-06 18:24:47 +03:00
|
|
|
, _failures :: [(Either Failure a, Trace)]
|
2015-01-23 19:48:38 +03:00
|
|
|
-- ^ The failed cases, if any.
|
|
|
|
} deriving (Show, Eq)
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-01-23 19:48:38 +03:00
|
|
|
instance NFData a => NFData (Result a) where
|
2015-06-19 18:50:51 +03:00
|
|
|
rnf r = rnf (_pass r, _casesChecked r, _failures r)
|
2015-01-23 19:48:38 +03:00
|
|
|
|
|
|
|
instance Functor Result where
|
|
|
|
fmap f r = r { _failures = map (first $ fmap f) $ _failures r }
|
2015-01-19 14:50:43 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Run a predicate over all executions with two or fewer
|
2015-02-04 14:45:08 +03:00
|
|
|
-- pre-emptions. A pre-emption is a context switch where the old
|
|
|
|
-- thread was still runnable.
|
2015-02-01 04:21:42 +03:00
|
|
|
--
|
|
|
|
-- 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\").
|
2015-01-30 18:16:15 +03:00
|
|
|
runTest :: Eq a => Predicate a -> (forall t. Conc t a) -> Result a
|
2015-01-12 20:52:30 +03:00
|
|
|
runTest = runTest' 2
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Variant of 'runTest' for computations which do 'IO'.
|
|
|
|
runTestIO :: Eq a => Predicate a -> (forall t. ConcIO t a) -> IO (Result a)
|
2015-01-12 20:52:30 +03:00
|
|
|
runTestIO = runTestIO' 2
|
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Variant of 'runTest' which takes a pre-emption bound.
|
2015-01-30 18:16:15 +03:00
|
|
|
runTest' :: Eq a => Int -> Predicate a -> (forall t. Conc t a) -> Result a
|
2015-07-17 00:32:30 +03:00
|
|
|
runTest' pb predicate conc = predicate $ sctPreBound pb conc
|
2015-01-12 20:52:30 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Variant of 'runTest'' for computations which do 'IO'.
|
2015-02-27 00:57:55 +03:00
|
|
|
runTestIO' :: Eq a => Int -> Predicate a -> (forall t. ConcIO t a) -> IO (Result a)
|
2015-07-17 00:32:30 +03:00
|
|
|
runTestIO' pb predicate conc = predicate <$> sctPreBoundIO pb conc
|
2015-02-04 14:45:08 +03:00
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Predicates
|
|
|
|
|
|
|
|
-- | A @Predicate@ is a function which collapses a list of results
|
|
|
|
-- into a 'Result'.
|
2015-07-17 00:32:30 +03:00
|
|
|
type Predicate a = [(Either Failure a, Trace)] -> Result a
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- | Check that a computation never deadlocks.
|
|
|
|
deadlocksNever :: Predicate a
|
2015-02-17 08:58:25 +03:00
|
|
|
deadlocksNever = alwaysTrue (not . either (`elem` [Deadlock, STMDeadlock]) (const False))
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- | Check that a computation always deadlocks.
|
|
|
|
deadlocksAlways :: Predicate a
|
2015-02-17 08:58:25 +03:00
|
|
|
deadlocksAlways = alwaysTrue $ either (`elem` [Deadlock, STMDeadlock]) (const False)
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-01-15 05:16:02 +03:00
|
|
|
-- | Check that a computation deadlocks at least once.
|
|
|
|
deadlocksSometimes :: Predicate a
|
2015-02-17 08:58:25 +03:00
|
|
|
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)
|
2015-01-15 05:16:02 +03:00
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- | 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
|
2015-01-12 22:12:20 +03:00
|
|
|
alwaysSame = alwaysTrue2 (==)
|
2015-01-12 22:08:09 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | Check that the result of a computation is not always the same.
|
|
|
|
notAlwaysSame :: Eq a => Predicate a
|
2015-07-17 00:32:30 +03:00
|
|
|
notAlwaysSame [x] = Result { _pass = False, _casesChecked = 1, _failures = [x] }
|
|
|
|
notAlwaysSame xs = go xs Result { _pass = False, _casesChecked = 0, _failures = [] } where
|
|
|
|
go [y1,y2] res
|
|
|
|
| fst y1 /= fst y2 = incCC res { _pass = True }
|
|
|
|
| otherwise = incCC res { _failures = y1 : y2 : _failures res }
|
|
|
|
go (y1:y2:ys) res
|
|
|
|
| fst y1 /= fst y2 = go (y2:ys) . incCC $ res { _pass = True }
|
|
|
|
| otherwise = go (y2:ys) . incCC $ res { _failures = y1 : y2 : _failures res }
|
|
|
|
go _ res = res
|
2015-02-01 04:21:42 +03:00
|
|
|
|
2015-01-15 05:16:02 +03:00
|
|
|
-- | Check that the result of a unary boolean predicate is always
|
2015-02-01 04:21:42 +03:00
|
|
|
-- true.
|
2015-02-06 18:24:47 +03:00
|
|
|
alwaysTrue :: (Either Failure a -> Bool) -> Predicate a
|
2015-07-17 00:32:30 +03:00
|
|
|
alwaysTrue p xs = go xs Result { _pass = True, _casesChecked = 0, _failures = filter (not . p . fst) xs } where
|
|
|
|
go (y:ys) res
|
|
|
|
| p (fst y) = go ys . incCC $ res
|
|
|
|
| otherwise = incCC $ res { _pass = False }
|
2015-01-23 19:48:38 +03:00
|
|
|
go [] res = res
|
|
|
|
|
2015-06-19 18:50:51 +03:00
|
|
|
-- | 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.
|
2015-01-23 19:48:38 +03:00
|
|
|
--
|
2015-02-01 04:21:42 +03:00
|
|
|
-- If the predicate fails, /both/ (result,trace) tuples will be added
|
2015-01-23 19:48:38 +03:00
|
|
|
-- to the failures list.
|
2015-02-06 18:24:47 +03:00
|
|
|
alwaysTrue2 :: (Either Failure a -> Either Failure a -> Bool) -> Predicate a
|
2015-07-08 20:20:08 +03:00
|
|
|
alwaysTrue2 _ [_] = Result { _pass = True, _casesChecked = 1, _failures = [] }
|
2015-07-17 00:32:30 +03:00
|
|
|
alwaysTrue2 p xs = go xs Result { _pass = True, _casesChecked = 0, _failures = failures xs } where
|
|
|
|
go [y1,y2] res
|
|
|
|
| p (fst y1) (fst y2) = incCC res
|
|
|
|
| otherwise = incCC res { _pass = False }
|
|
|
|
go (y1:y2:ys) res
|
|
|
|
| p (fst y1) (fst y2) = go (y2:ys) . incCC $ res
|
|
|
|
| otherwise = go (y2:ys) . incCC $ res { _pass = False }
|
|
|
|
go _ res = res
|
|
|
|
|
|
|
|
failures (y1:y2:ys)
|
|
|
|
| p (fst y1) (fst y2) = failures (y2:ys)
|
|
|
|
| otherwise = y1 : if null ys then [y2] else failures (y2:ys)
|
|
|
|
failures _ = []
|
|
|
|
|
|
|
|
-- alwaysTrue2 almost certainly reports the number of cases checked incorrectly.
|
2015-01-15 05:16:02 +03:00
|
|
|
|
|
|
|
-- | Check that the result of a unary boolean predicate is true at
|
2015-02-01 04:21:42 +03:00
|
|
|
-- least once.
|
2015-02-06 18:24:47 +03:00
|
|
|
somewhereTrue :: (Either Failure a -> Bool) -> Predicate a
|
2015-07-17 00:32:30 +03:00
|
|
|
somewhereTrue p xs = go xs Result { _pass = False, _casesChecked = 0, _failures = filter (not . p . fst) xs } where
|
|
|
|
go (y:ys) res
|
|
|
|
| p (fst y) = incCC $ res { _pass = True }
|
|
|
|
| otherwise = go ys . incCC $ res { _failures = y : _failures res }
|
2015-01-15 05:16:02 +03:00
|
|
|
go [] res = res
|
2015-01-23 19:48:38 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- * Internal
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
-- | 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) ++ ")")
|
2015-02-02 00:55:25 +03:00
|
|
|
|
|
|
|
let failures = _failures result
|
2015-02-06 18:24:47 +03:00
|
|
|
mapM_ (\(r, t) -> putStrLn $ "\t" ++ either showfail show r ++ " " ++ showTrace t) $ take 5 failures
|
2015-02-01 04:21:42 +03:00
|
|
|
when (moreThan failures 5) $
|
|
|
|
putStrLn "\t..."
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-02-01 04:21:42 +03:00
|
|
|
return $ _pass result
|
2015-01-15 05:16:02 +03:00
|
|
|
|
2015-06-19 18:50:51 +03:00
|
|
|
-- | Increment the cases
|
2015-01-23 19:48:38 +03:00
|
|
|
incCC :: Result a -> Result a
|
2015-01-15 05:16:02 +03:00
|
|
|
incCC r = r { _casesChecked = _casesChecked r + 1 }
|
2015-01-23 19:48:38 +03:00
|
|
|
|
2015-02-06 18:24:47 +03:00
|
|
|
-- | Pretty-print a failure
|
|
|
|
showfail :: Failure -> String
|
2015-02-12 22:15:07 +03:00
|
|
|
showfail Deadlock = "[deadlock]"
|
|
|
|
showfail STMDeadlock = "[stm-deadlock]"
|
|
|
|
showfail InternalError = "[internal-error]"
|
|
|
|
showfail FailureInNoTest = "[_concNoTest]"
|
|
|
|
showfail UncaughtException = "[exception]"
|