2015-01-21 18:31:10 +03:00
|
|
|
{-# LANGUAGE Rank2Types #-}
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- | Useful functions for writing SCT test cases for @Conc@
|
|
|
|
-- computations.
|
|
|
|
module Control.Monad.Conc.SCT.Tests
|
2015-01-25 19:15:23 +03:00
|
|
|
( doTests
|
2015-01-26 20:36:25 +03:00
|
|
|
, doTests'
|
2015-01-23 20:06:14 +03:00
|
|
|
-- * Test cases
|
|
|
|
, Result(..)
|
2015-01-12 20:27:41 +03:00
|
|
|
, runTest
|
|
|
|
, runTestIO
|
2015-01-12 20:52:30 +03:00
|
|
|
, runTest'
|
|
|
|
, runTestIO'
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Predicates
|
|
|
|
, Predicate
|
|
|
|
, deadlocksNever
|
|
|
|
, deadlocksAlways
|
2015-01-15 05:16:02 +03:00
|
|
|
, deadlocksSometimes
|
2015-01-12 20:27:41 +03:00
|
|
|
, alwaysSame
|
2015-01-12 22:08:09 +03:00
|
|
|
, alwaysTrue
|
|
|
|
, alwaysTrue2
|
2015-01-15 05:16:02 +03:00
|
|
|
, somewhereTrue
|
|
|
|
, somewhereTrue2
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Utilities
|
|
|
|
, pAnd
|
|
|
|
, pNot
|
2015-01-23 19:48:38 +03:00
|
|
|
, rForgetful
|
2015-01-12 20:27:41 +03:00
|
|
|
) where
|
|
|
|
|
|
|
|
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-01-23 20:06:14 +03:00
|
|
|
import Control.Monad (when, void)
|
2015-01-12 20:27:41 +03:00
|
|
|
import Control.Monad.Conc.Fixed
|
2015-01-23 19:48:38 +03:00
|
|
|
import Control.Monad.Conc.SCT.Internal
|
2015-01-26 18:34:40 +03:00
|
|
|
import Control.Monad.Conc.SCT.Bounding
|
2015-01-27 16:46:20 +03:00
|
|
|
import Data.List.Extra
|
2015-01-12 20:27:41 +03:00
|
|
|
import Data.Maybe (isJust, isNothing)
|
|
|
|
|
|
|
|
import qualified Control.Monad.Conc.Fixed.IO as CIO
|
|
|
|
|
2015-01-23 20:06:14 +03:00
|
|
|
-- * Test suites
|
|
|
|
|
|
|
|
-- | Run a collection of tests (with a pb of 2), printing results to
|
|
|
|
-- stdout, and returning 'True' iff all tests pass.
|
2015-01-26 20:36:25 +03:00
|
|
|
doTests :: Show a
|
|
|
|
=> Bool
|
2015-01-23 20:06:14 +03:00
|
|
|
-- ^ Whether to print test passes.
|
2015-01-23 20:12:34 +03:00
|
|
|
-> [(String, Result a)]
|
2015-01-23 20:06:14 +03:00
|
|
|
-- ^ The test cases
|
|
|
|
-> IO Bool
|
2015-01-26 20:36:25 +03:00
|
|
|
doTests = doTests' show
|
|
|
|
|
|
|
|
-- | Variant of 'doTests' which takes a result printing function.
|
|
|
|
doTests' :: (a -> String) -> Bool -> [(String, Result a)] -> IO Bool
|
|
|
|
doTests' showf verbose tests = do
|
|
|
|
results <- mapM (doTest showf verbose) tests
|
2015-01-23 20:06:14 +03:00
|
|
|
return $ and results
|
|
|
|
|
|
|
|
-- | Run a test and print to stdout
|
2015-01-26 20:36:25 +03:00
|
|
|
doTest :: (a -> String) -> Bool -> (String, Result a) -> IO Bool
|
|
|
|
doTest showf verbose (name, result) = do
|
2015-01-23 20:06:14 +03:00
|
|
|
if _pass result
|
|
|
|
then
|
|
|
|
-- If verbose, display a pass message.
|
|
|
|
when verbose $
|
|
|
|
putStrLn $ "\27[32m[pass]\27[0m " ++ name ++ " (checked: " ++ show (_casesChecked result) ++ ")"
|
|
|
|
else do
|
2015-01-26 20:36:25 +03:00
|
|
|
-- Display a failure message, and the first 5 (simplified) failed traces
|
2015-01-23 20:06:14 +03:00
|
|
|
putStrLn ("\27[31m[fail]\27[0m " ++ name ++ " (checked: " ++ show (_casesChecked result) ++ ")")
|
2015-01-26 20:36:25 +03:00
|
|
|
let traces = let (rs, ts) = unzip . take 5 $ _failures result in rs `zip` simplify ts
|
|
|
|
mapM_ (\(r, t) -> putStrLn $ "\t" ++ maybe "[deadlock]" showf r ++ " " ++ showtrc t) traces
|
|
|
|
when (length (_failures result) > 5) $
|
2015-01-23 20:06:14 +03:00
|
|
|
putStrLn "\t..."
|
|
|
|
|
|
|
|
return $ _pass result
|
|
|
|
|
2015-01-26 20:36:25 +03:00
|
|
|
-- | Simplify a collection of traces, by attempting to factor out
|
|
|
|
-- common prefixes and suffixes.
|
|
|
|
simplify :: [SCTTrace] -> [(SCTTrace, SCTTrace, SCTTrace)]
|
|
|
|
simplify [t] = [([], t, [])]
|
|
|
|
simplify ts = map (\t -> (pref, drop plen $ take (length t - slen) t, suff)) ts where
|
|
|
|
pref = commonPrefix ts
|
|
|
|
plen = length pref
|
|
|
|
suff = commonSuffix ts
|
|
|
|
slen = length suff
|
|
|
|
|
|
|
|
-- | Pretty-print a simplified trace
|
|
|
|
showtrc :: (SCTTrace, SCTTrace, SCTTrace) -> String
|
|
|
|
showtrc (p, t, s) = case (p, s) of
|
|
|
|
([], []) -> hilight ++ showtrc' t ++ reset
|
|
|
|
([], _) -> hilight ++ showtrc' t ++ reset ++ s'
|
|
|
|
(_, []) -> p' ++ hilight ++ showtrc' t ++ reset
|
|
|
|
(_, _) -> p' ++ hilight ++ showtrc' t ++ reset ++ s'
|
|
|
|
|
|
|
|
where
|
|
|
|
showtrc' = showTrace . map (\(d,as,_) -> (d,as))
|
|
|
|
hilight = "\27[33m"
|
|
|
|
reset = "\27[0m"
|
|
|
|
p' = (if length p > 25 then ("..." ++) . reverse . take 25 . reverse else id) $ showtrc' p
|
|
|
|
s' = (if length s > 25 then (++ "...") . take 25 else id) $ showtrc' s
|
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Test cases
|
|
|
|
|
|
|
|
-- | The results of a test, including information on the number of
|
|
|
|
-- cases checked, and number of total cases. Be careful if using the
|
|
|
|
-- total number of cases, as that value may be very big, and (due to
|
|
|
|
-- laziness) will actually force a lot more computation!.
|
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.
|
|
|
|
, _casesTotal :: Int
|
|
|
|
-- ^ The total number of cases.
|
2015-01-23 19:48:38 +03:00
|
|
|
, _failures :: [(Maybe a, SCTTrace)]
|
|
|
|
-- ^ 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
|
|
|
|
rnf r = rnf (_pass r, _casesChecked r, _casesTotal r, _failures r)
|
|
|
|
|
|
|
|
instance Functor Result where
|
|
|
|
fmap f r = r { _failures = map (first $ fmap f) $ _failures r }
|
2015-01-19 14:50:43 +03:00
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- | Run a test using the pre-emption bounding scheduler, with a bound
|
|
|
|
-- of 2.
|
2015-01-23 19:48:38 +03:00
|
|
|
runTest :: 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
|
|
|
|
|
|
|
-- | Variant of 'runTest' using 'IO'. See usual caveats about 'IO'.
|
2015-01-23 19:48:38 +03:00
|
|
|
runTestIO :: Predicate a -> (forall t. CIO.Conc t a) -> IO (Result a)
|
2015-01-12 20:52:30 +03:00
|
|
|
runTestIO = runTestIO' 2
|
|
|
|
|
|
|
|
-- | Run a test using the pre-emption bounding scheduler.
|
2015-01-23 19:48:38 +03:00
|
|
|
runTest' :: Int -> Predicate a -> (forall t. Conc t a) -> Result a
|
|
|
|
runTest' pb predicate conc = predicate $ sctPreBound pb conc
|
2015-01-12 20:52:30 +03:00
|
|
|
|
|
|
|
-- | Variant of 'runTest'' using 'IO'. See usual caveats about 'IO'.
|
2015-01-23 19:48:38 +03:00
|
|
|
runTestIO' :: Int -> Predicate a -> (forall t. CIO.Conc t a) -> IO (Result a)
|
|
|
|
runTestIO' pb predicate conc = predicate <$> sctPreBoundIO pb conc
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- * Predicates
|
|
|
|
|
|
|
|
-- | A @Predicate@ is a function which collapses a list of results
|
|
|
|
-- into a 'Result'.
|
2015-01-23 19:48:38 +03:00
|
|
|
type Predicate a = [(Maybe a, SCTTrace)] -> Result a
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- | Check that a computation never deadlocks.
|
|
|
|
deadlocksNever :: Predicate a
|
2015-01-12 22:08:09 +03:00
|
|
|
deadlocksNever = alwaysTrue isJust
|
2015-01-12 20:27:41 +03:00
|
|
|
|
|
|
|
-- | Check that a computation always deadlocks.
|
|
|
|
deadlocksAlways :: Predicate a
|
2015-01-12 22:08:09 +03:00
|
|
|
deadlocksAlways = alwaysTrue isNothing
|
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
|
|
|
|
deadlocksSometimes = somewhereTrue isNothing
|
|
|
|
|
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-01-15 05:16:02 +03:00
|
|
|
-- | Check that the result of a unary boolean predicate is always
|
|
|
|
-- true. An empty list of results counts as 'True'.
|
2015-01-12 22:08:09 +03:00
|
|
|
alwaysTrue :: (Maybe a -> Bool) -> Predicate a
|
2015-01-23 19:48:38 +03:00
|
|
|
alwaysTrue p xs = go xs Result { _pass = True, _casesChecked = 0, _casesTotal = len, _failures = failures } where
|
|
|
|
go [] res = res
|
|
|
|
go ((y,_):ys) res
|
|
|
|
| p y = go ys $ incCC res
|
|
|
|
| otherwise = incCC res { _pass = False }
|
|
|
|
|
|
|
|
(len, failures) = findFailures1 p xs
|
2015-01-12 22:08:09 +03:00
|
|
|
|
|
|
|
-- | Check that the result of a binary boolean predicate is always
|
2015-01-15 05:16:02 +03:00
|
|
|
-- true between adjacent pairs of results. An empty list of results
|
|
|
|
-- counts as 'True'.
|
2015-01-23 19:48:38 +03:00
|
|
|
--
|
|
|
|
-- If the predicate fails, *both* (result,trace) tuples will be added
|
|
|
|
-- to the failures list.
|
2015-01-12 22:12:20 +03:00
|
|
|
alwaysTrue2 :: (Maybe a -> Maybe a -> Bool) -> Predicate a
|
2015-01-23 19:48:38 +03:00
|
|
|
alwaysTrue2 _ [_] = Result { _pass = True, _casesChecked = 1, _casesTotal = 1, _failures = [] }
|
|
|
|
alwaysTrue2 p xs = go xs Result { _pass = True, _casesChecked = 0, _casesTotal = len, _failures = failures } where
|
|
|
|
go [] = id
|
|
|
|
go [(y1,_),(y2,_)] = check y1 y2 []
|
|
|
|
go ((y1,_):(y2,t):ys) = check y1 y2 ((y2,t) : ys)
|
|
|
|
|
|
|
|
check y1 y2 ys res
|
|
|
|
| p y1 y2 = go ys $ incCC res
|
|
|
|
| otherwise = incCC res { _pass = False }
|
|
|
|
|
|
|
|
(len, failures) = findFailures2 p xs
|
2015-01-15 05:16:02 +03:00
|
|
|
|
|
|
|
-- | Check that the result of a unary boolean predicate is true at
|
|
|
|
-- least once. An empty list of results counts as 'False'.
|
|
|
|
somewhereTrue :: (Maybe a -> Bool) -> Predicate a
|
2015-01-23 19:48:38 +03:00
|
|
|
somewhereTrue p xs = go xs Result { _pass = False, _casesChecked = 0, _casesTotal = len, _failures = failures } where
|
2015-01-15 05:16:02 +03:00
|
|
|
go [] res = res
|
2015-01-23 19:48:38 +03:00
|
|
|
go ((y,_):ys) res
|
2015-01-15 05:16:02 +03:00
|
|
|
| p y = incCC res { _pass = True }
|
|
|
|
| otherwise = go ys $ incCC res
|
|
|
|
|
2015-01-23 19:48:38 +03:00
|
|
|
(len, failures) = findFailures1 p xs
|
|
|
|
|
2015-01-15 05:16:02 +03:00
|
|
|
-- | Check that the result of a binary boolean predicate is true
|
|
|
|
-- between at least one adjacent pair of results. An empty list of
|
|
|
|
-- results counts as 'False'.
|
2015-01-23 19:48:38 +03:00
|
|
|
--
|
|
|
|
-- If the predicate fails, *both* (result,trace) tuples will be added
|
|
|
|
-- to the failures list.
|
2015-01-15 05:16:02 +03:00
|
|
|
somewhereTrue2 :: (Maybe a -> Maybe a -> Bool) -> Predicate a
|
2015-01-23 19:48:38 +03:00
|
|
|
somewhereTrue2 _ [x] = Result { _pass = False, _casesChecked = 1, _casesTotal = 1, _failures = [x] }
|
|
|
|
somewhereTrue2 p xs = go xs Result { _pass = False, _casesChecked = 0, _casesTotal = len, _failures = failures } where
|
|
|
|
go [] = id
|
|
|
|
go [(y1,_),(y2,_)] = check y1 y2 []
|
|
|
|
go ((y1,_):(y2,t):ys) = check y1 y2 ((y2,t) : ys)
|
2015-01-12 22:08:09 +03:00
|
|
|
|
|
|
|
check y1 y2 ys res
|
2015-01-15 05:16:02 +03:00
|
|
|
| p y1 y2 = incCC res { _pass = True }
|
|
|
|
| otherwise = go ys $ incCC res
|
2015-01-12 20:27:41 +03:00
|
|
|
|
2015-01-23 19:48:38 +03:00
|
|
|
(len, failures) = findFailures2 p xs
|
|
|
|
|
2015-01-12 20:27:41 +03:00
|
|
|
-- * Utils
|
|
|
|
|
|
|
|
-- | Compose two predicates sequentially.
|
|
|
|
pAnd :: Predicate a -> Predicate a -> Predicate a
|
|
|
|
pAnd p q xs = if _pass r1 then r2 else r1 where
|
|
|
|
r1 = p xs
|
|
|
|
r2 = q xs
|
|
|
|
|
|
|
|
-- | Invert the result of a predicate.
|
|
|
|
pNot :: Predicate a -> Predicate a
|
|
|
|
pNot p xs = r { _pass = not $ _pass r } where
|
|
|
|
r = p xs
|
2015-01-15 05:16:02 +03:00
|
|
|
|
2015-01-23 19:48:38 +03:00
|
|
|
-- | Throw away the failures information in a Result (useful for
|
|
|
|
-- storing them in a list).
|
|
|
|
rForgetful :: Result a -> Result ()
|
|
|
|
rForgetful = void
|
|
|
|
|
2015-01-15 05:16:02 +03:00
|
|
|
-- | Increment the cases checked
|
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
|
|
|
|
|
|
|
-- | Get the length of the list and find the failing cases in one
|
|
|
|
-- traversal.
|
|
|
|
findFailures1 :: (Maybe a -> Bool) -> [(Maybe a, SCTTrace)] -> (Int, [(Maybe a, SCTTrace)])
|
|
|
|
findFailures1 p xs = findFailures xs 0 [] where
|
|
|
|
findFailures [] l fs = (l, fs)
|
|
|
|
findFailures ((z,t):zs) l fs
|
|
|
|
| p z = findFailures zs (l+1) fs
|
|
|
|
| otherwise = findFailures zs (l+1) ((z,t):fs)
|
|
|
|
|
|
|
|
-- | Get the length of the list and find the failing cases in one
|
|
|
|
-- traversal.
|
|
|
|
findFailures2 :: (Maybe a -> Maybe a -> Bool) -> [(Maybe a, SCTTrace)] -> (Int, [(Maybe a, SCTTrace)])
|
|
|
|
findFailures2 p xs = findFailures xs 0 [] where
|
|
|
|
findFailures [] l fs = (l, fs)
|
|
|
|
findFailures [_] l fs = (l+1, fs)
|
|
|
|
findFailures ((z1,t1):(z2,t2):zs) l fs
|
|
|
|
| p z1 z2 = findFailures ((z2,t2):zs) (l+1) fs
|
|
|
|
| otherwise = findFailures ((z2,t2):zs) (l+1) ((z1,t1):(z2,t2):fs)
|