dejafu 0.6.0.0 → 0.7.0.0
raw patch · 10 files changed
+694/−154 lines, 10 filesdep +leancheckPVP ok
version bump matches the API change (PVP)
Dependencies added: leancheck
API changes (from Hackage documentation)
- Test.DejaFu: [Randomly] :: RandomGen g => g -> Int -> Way
- Test.DejaFu: [Systematically] :: Bounds -> Way
- Test.DejaFu.SCT: [Randomly] :: RandomGen g => g -> Int -> Way
- Test.DejaFu.SCT: [Systematically] :: Bounds -> Way
- Test.DejaFu.SCT: sctFairBound :: MonadRef r n => MemType -> FairBound -> ConcT r n a -> n [(Either Failure a, Trace)]
- Test.DejaFu.SCT: sctLengthBound :: MonadRef r n => MemType -> LengthBound -> ConcT r n a -> n [(Either Failure a, Trace)]
- Test.DejaFu.SCT: sctPreBound :: MonadRef r n => MemType -> PreemptionBound -> ConcT r n a -> n [(Either Failure a, Trace)]
- Test.DejaFu.SCT: sctRandom :: (MonadRef r n, RandomGen g) => MemType -> g -> Int -> ConcT r n a -> n [(Either Failure a, Trace)]
+ Test.DejaFu: randomly :: RandomGen g => g -> Int -> Way
+ Test.DejaFu: swarmy :: RandomGen g => g -> Int -> Int -> Way
+ Test.DejaFu: systematically :: Bounds -> Way
+ Test.DejaFu: uniformly :: RandomGen g => g -> Int -> Way
+ Test.DejaFu.Defaults: defaultBounds :: Bounds
+ Test.DejaFu.Defaults: defaultFairBound :: FairBound
+ Test.DejaFu.Defaults: defaultLengthBound :: LengthBound
+ Test.DejaFu.Defaults: defaultMemType :: MemType
+ Test.DejaFu.Defaults: defaultPreemptionBound :: PreemptionBound
+ Test.DejaFu.Defaults: defaultWay :: Way
+ Test.DejaFu.Refinement: (->-) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: (===) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: (=>=) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: CounterExample :: x -> [String] -> Set (Maybe Failure, o) -> Set (Maybe Failure, o) -> FailedProperty o x
+ Test.DejaFu.Refinement: NoExpectedFailure :: FailedProperty o x
+ Test.DejaFu.Refinement: Sig :: (x -> ConcIO s) -> (s -> x -> ConcIO o) -> (s -> x -> ConcIO ()) -> (s -> ConcIO ()) -> Sig s o x
+ Test.DejaFu.Refinement: [expression] :: Sig s o x -> s -> ConcIO ()
+ Test.DejaFu.Refinement: [failingArgs] :: FailedProperty o x -> [String]
+ Test.DejaFu.Refinement: [failingSeed] :: FailedProperty o x -> x
+ Test.DejaFu.Refinement: [initialise] :: Sig s o x -> x -> ConcIO s
+ Test.DejaFu.Refinement: [interfere] :: Sig s o x -> s -> x -> ConcIO ()
+ Test.DejaFu.Refinement: [leftResults] :: FailedProperty o x -> Set (Maybe Failure, o)
+ Test.DejaFu.Refinement: [observe] :: Sig s o x -> s -> x -> ConcIO o
+ Test.DejaFu.Refinement: [rightResults] :: FailedProperty o x -> Set (Maybe Failure, o)
+ Test.DejaFu.Refinement: check :: (Testable p, Listable (X p), Eq (X p), Show (X p), Show (O p)) => p -> IO Bool
+ Test.DejaFu.Refinement: check' :: (Testable p, Listable (X p), Eq (X p), Show (X p), Show (O p)) => p -> IO (Maybe (FailedProperty (O p) (X p)))
+ Test.DejaFu.Refinement: checkFor :: (Testable p, Listable (X p), Eq (X p), Show (X p)) => Int -> Int -> p -> IO (Maybe (FailedProperty (O p) (X p)))
+ Test.DejaFu.Refinement: class Listable a
+ Test.DejaFu.Refinement: class Testable a where type O a :: * type X a :: * where {
+ Test.DejaFu.Refinement: counterExamples :: (Testable p, Listable (X p), Eq (X p)) => Int -> Int -> p -> IO [FailedProperty (O p) (X p)]
+ Test.DejaFu.Refinement: data FailedProperty o x
+ Test.DejaFu.Refinement: data RefinementProperty o x
+ Test.DejaFu.Refinement: data Sig s o x
+ Test.DejaFu.Refinement: equivalentTo :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: expectFailure :: RefinementProperty o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: instance (GHC.Show.Show o, GHC.Show.Show x) => GHC.Show.Show (Test.DejaFu.Refinement.FailedProperty o x)
+ Test.DejaFu.Refinement: instance (Test.LeanCheck.Core.Listable a, GHC.Show.Show a, Test.DejaFu.Refinement.Testable b) => Test.DejaFu.Refinement.Testable (a -> b)
+ Test.DejaFu.Refinement: instance GHC.Classes.Eq Test.DejaFu.Refinement.How
+ Test.DejaFu.Refinement: instance Test.DejaFu.Refinement.Testable (Test.DejaFu.Refinement.RefinementProperty o x)
+ Test.DejaFu.Refinement: list :: Listable a => [a]
+ Test.DejaFu.Refinement: refines :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: strictlyRefines :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x
+ Test.DejaFu.Refinement: tiers :: Listable a => [[a]]
+ Test.DejaFu.Refinement: type family X a :: *;
+ Test.DejaFu.Refinement: }
+ Test.DejaFu.SCT: randomly :: RandomGen g => g -> Int -> Way
+ Test.DejaFu.SCT: sctUniformRandom :: (MonadRef r n, RandomGen g) => MemType -> g -> Int -> ConcT r n a -> n [(Either Failure a, Trace)]
+ Test.DejaFu.SCT: sctWeightedRandom :: (MonadRef r n, RandomGen g) => MemType -> g -> Int -> Int -> ConcT r n a -> n [(Either Failure a, Trace)]
+ Test.DejaFu.SCT: swarmy :: RandomGen g => g -> Int -> Int -> Way
+ Test.DejaFu.SCT: systematically :: Bounds -> Way
+ Test.DejaFu.SCT: uniformly :: RandomGen g => g -> Int -> Way
- Test.DejaFu.Conc.Internal: runConcurrency :: MonadRef r n => Scheduler g -> MemType -> g -> Int -> M n r a -> n (Either Failure a, g, SeqTrace, Maybe (ThreadId, ThreadAction))
+ Test.DejaFu.Conc.Internal: runConcurrency :: MonadRef r n => Scheduler g -> MemType -> g -> IdSource -> Int -> M n r a -> n (Either Failure a, Context n r g, SeqTrace, Maybe (ThreadId, ThreadAction))
- Test.DejaFu.SCT.Internal: initialRandSchedState :: g -> RandSchedState g
+ Test.DejaFu.SCT.Internal: initialRandSchedState :: Maybe (Map ThreadId Int) -> g -> RandSchedState g
- Test.DejaFu.SCT.Internal: randSched :: RandomGen g => Scheduler (RandSchedState g)
+ Test.DejaFu.SCT.Internal: randSched :: RandomGen g => (g -> (Int, g)) -> Scheduler (RandSchedState g)
Files
- CHANGELOG.markdown +47/−0
- Test/DejaFu.hs +41/−63
- Test/DejaFu/Conc.hs +2/−2
- Test/DejaFu/Conc/Internal.hs +15/−7
- Test/DejaFu/Conc/Internal/Memory.hs +1/−1
- Test/DejaFu/Defaults.hs +63/−0
- Test/DejaFu/Refinement.hs +389/−0
- Test/DejaFu/SCT.hs +123/−70
- Test/DejaFu/SCT/Internal.hs +8/−9
- dejafu.cabal +5/−2
CHANGELOG.markdown view
@@ -7,6 +7,53 @@ *de facto* standard Haskell versioning scheme. +0.7.0.0 [2017-06-07] (git tag: [dejafu-0.7.0.0][])+-------++https://hackage.haskell.org/package/dejafu-0.7.0.0++### Test.DejaFu++- The new Test.DejaFu.Defaults and Test.DejaFu.Refinement modules are re-exported.+- The new smart constructors from Test.DejaFu.SCT are exported.++### Test.DejaFu.Defaults++- The `default*` values are now defined in the new Test.DejaFu.Defaults module. There is no breaking+ API change as they are re-exported from Test.DejaFu.++### Test.DejaFu.Refinement++- A new module for checking observational properties of the side-effects of stateful, concurrent+ functions.++ This is related to my work on [CoCo][], allowing dejafu to test what CoCo discovers.++### Test.DejaFu.SCT++- The `Way` type is now abstract and exposes smart constructor functions:+ - `systematically`, corresponding to the old `Systematically`.+ - `randomly`, corresponding to the old `Randomly`,+ - `uniformly`, a new uniform random (as opposed to weighted random) scheduler.+ - `swarmy`, corresponding to the old `Randomly` and specifying how many executions to use the+ same weights for.+- A new `sctUniformRandom` function to do uniform (non-weighted) scheduling.+- The `sctRandom` function is now called `sctWeightedRandom` and can now re-use the same weights for+ multiple executions.+- The `sctPreBound`, `sctFairBound`, and `sctLengthBound` functions have been removed.++### Fixed++- An issue where `subconcurrency` would re-use `MVar` IDs, leading to false reports of deadlock on+ occasion (issue #81).++[CoCo]: https://github.com/barrucadu/coco+[dejafu-0.7.0.0]: https://github.com/barrucadu/dejafu/releases/tag/dejafu-0.7.0.0+++---------------------------------------------------------------------------------------------------++ 0.6.0.0 [2017-04-08] (git tag: [dejafu-0.6.0.0][]) -------
Test/DejaFu.hs view
@@ -93,8 +93,12 @@ -- * Testing with different settings - , Way(..)+ , Way , defaultWay+ , systematically+ , randomly+ , uniformly+ , swarmy , autocheckWay , autocheckWayIO@@ -240,19 +244,47 @@ , somewhereTrue , gives , gives'++ -- * Refinement property testing++ -- | Consider this statement about @MVar@s: \"using @readMVar@ is+ -- better than @takeMVar@ followed by @putMVar@ because the former+ -- is atomic but the latter is not.\"+ --+ -- Deja Fu can test properties like that:+ --+ -- @+ -- sig e = Sig+ -- { initialise = maybe newEmptyMVar newMVar+ -- , observe = \\v _ -> tryReadMVar v+ -- , interfere = \\v s -> tryTakeMVar v >> maybe (pure ()) (void . tryPutMVar v) s+ -- , expression = e+ -- }+ --+ -- > check $ sig (void . readMVar) \`equivalentTo\` sig (\\v -> takeMVar v >>= putMVar v)+ -- *** Failure: (seed Just ())+ -- left: [(Nothing,Just ())]+ -- right: [(Nothing,Just ()),(Just Deadlock,Just ())]+ -- @+ --+ -- The two expressions are not equivalent, and we get given the+ -- counterexample!+ , module Test.DejaFu.Refinement ) where -import Control.Arrow (first)-import Control.DeepSeq (NFData(..))-import Control.Monad (unless, when)-import Control.Monad.Ref (MonadRef)-import Control.Monad.ST (runST)-import Data.Function (on)-import Data.List (intercalate, intersperse, minimumBy)-import Data.Ord (comparing)+import Control.Arrow (first)+import Control.DeepSeq (NFData(..))+import Control.Monad (unless, when)+import Control.Monad.Ref (MonadRef)+import Control.Monad.ST (runST)+import Data.Function (on)+import Data.List (intercalate, intersperse, minimumBy)+import Data.Ord (comparing) import Test.DejaFu.Common import Test.DejaFu.Conc+import Test.DejaFu.Defaults+import Test.DejaFu.Refinement import Test.DejaFu.SCT @@ -687,60 +719,6 @@ -- @since 0.2.0.0 gives' :: (Eq a, Show a) => [a] -> Predicate a gives' = gives . map Right------------------------------------------------------------------------------------- Defaults---- | A default way to execute concurrent programs: systematically--- using 'defaultBounds'.------ @since 0.6.0.0-defaultWay :: Way-defaultWay = Systematically defaultBounds---- | The default memory model: @TotalStoreOrder@------ @since 0.2.0.0-defaultMemType :: MemType-defaultMemType = TotalStoreOrder---- | All bounds enabled, using their default values.------ @since 0.2.0.0-defaultBounds :: Bounds-defaultBounds = Bounds- { boundPreemp = Just defaultPreemptionBound- , boundFair = Just defaultFairBound- , boundLength = Just defaultLengthBound- }---- | A sensible default preemption bound: 2.------ See /Concurrency Testing Using Schedule Bounding: an Empirical Study/,--- P. Thomson, A. F. Donaldson, A. Betts for justification.------ @since 0.2.0.0-defaultPreemptionBound :: PreemptionBound-defaultPreemptionBound = 2---- | A sensible default fair bound: 5.------ This comes from playing around myself, but there is probably a--- better default.------ @since 0.2.0.0-defaultFairBound :: FairBound-defaultFairBound = 5---- | A sensible default length bound: 250.------ Based on the assumption that anything which executes for much--- longer (or even this long) will take ages to test.------ @since 0.2.0.0-defaultLengthBound :: LengthBound-defaultLengthBound = 250 -------------------------------------------------------------------------------
Test/DejaFu/Conc.hs view
@@ -198,8 +198,8 @@ -> ConcT r n a -> n (Either Failure a, s, Trace) runConcurrent sched memtype s ma = do- (res, s', trace, _) <- runConcurrency sched memtype s 2 (unC ma)- pure (res, s', F.toList trace)+ (res, ctx, trace, _) <- runConcurrency sched memtype s initialIdSource 2 (unC ma)+ pure (res, cSchedState ctx, F.toList trace) -- | Run a concurrent computation and return its result. --
Test/DejaFu/Conc/Internal.hs view
@@ -52,19 +52,24 @@ => Scheduler g -> MemType -> g+ -> IdSource -> Int -> M n r a- -> n (Either Failure a, g, SeqTrace, Maybe (ThreadId, ThreadAction))-runConcurrency sched memtype g caps ma = do+ -> n (Either Failure a, Context n r g, SeqTrace, Maybe (ThreadId, ThreadAction))+runConcurrency sched memtype g idsrc caps ma = do ref <- newRef Nothing let c = runCont ma (AStop . writeRef ref . Just . Right)- let threads = launch' Unmasked initialThread (const c) M.empty- let ctx = Context { cSchedState = g, cIdSource = initialIdSource, cThreads = threads, cWriteBuf = emptyBuffer, cCaps = caps }+ let ctx = Context { cSchedState = g+ , cIdSource = idsrc+ , cThreads = launch' Unmasked initialThread (const c) M.empty+ , cWriteBuf = emptyBuffer+ , cCaps = caps+ } (finalCtx, trace, finalAction) <- runThreads sched memtype ref ctx out <- readRef ref- pure (fromJust out, cSchedState finalCtx, trace, finalAction)+ pure (fromJust out, finalCtx, trace, finalAction) -- | The context a collection of threads are running in. data Context n r g = Context@@ -369,8 +374,11 @@ ASub ma c | M.size (cThreads ctx) > 1 -> pure (Left IllegalSubconcurrency, Single Subconcurrency) | otherwise -> do- (res, g', trace, finalDecision) <- runConcurrency sched memtype (cSchedState ctx) (cCaps ctx) ma- pure (Right ctx { cThreads = goto (AStopSub (c res)) tid (cThreads ctx), cSchedState = g' }, SubC trace finalDecision)+ (res, ctx', trace, finalDecision) <-+ runConcurrency sched memtype (cSchedState ctx) (cIdSource ctx) (cCaps ctx) ma+ pure (Right ctx { cThreads = goto (AStopSub (c res)) tid (cThreads ctx)+ , cIdSource = cIdSource ctx'+ , cSchedState = cSchedState ctx' }, SubC trace finalDecision) -- after the end of a subconcurrent computation. does nothing, -- only exists so that: there is an entry in the trace for
Test/DejaFu/Conc/Internal/Memory.hs view
@@ -131,7 +131,7 @@ addCommitThreads :: WriteBuffer r -> Threads n r -> Threads n r addCommitThreads (WriteBuffer wb) ts = ts <> M.fromList phantoms where phantoms = [ (ThreadId Nothing $ negate tid, mkthread $ fromJust c)- | ((k, b), tid) <- zip (M.toList wb) [1..]+ | ((_, b), tid) <- zip (M.toList wb) [1..] , let c = go $ viewl b , isJust c] go (BufferedWrite tid (CRef crid _) _ :< _) = Just $ ACommit tid crid
+ Test/DejaFu/Defaults.hs view
@@ -0,0 +1,63 @@+-- |+-- Module : Test.DejaFu.Defaults+-- Copyright : (c) 2017 Michael Walker+-- License : MIT+-- Maintainer : Michael Walker <mike@barrucadu.co.uk>+-- Stability : experimental+-- Portability : portable+--+-- Default parameters for test execution.+module Test.DejaFu.Defaults where++import Test.DejaFu.Common+import Test.DejaFu.SCT++-- | A default way to execute concurrent programs: systematically+-- using 'defaultBounds'.+--+-- @since 0.6.0.0+defaultWay :: Way+defaultWay = systematically defaultBounds++-- | The default memory model: @TotalStoreOrder@+--+-- @since 0.2.0.0+defaultMemType :: MemType+defaultMemType = TotalStoreOrder++-- | All bounds enabled, using their default values.+--+-- @since 0.2.0.0+defaultBounds :: Bounds+defaultBounds = Bounds+ { boundPreemp = Just defaultPreemptionBound+ , boundFair = Just defaultFairBound+ , boundLength = Just defaultLengthBound+ }++-- | A sensible default preemption bound: 2.+--+-- See /Concurrency Testing Using Schedule Bounding: an Empirical Study/,+-- P. Thomson, A. F. Donaldson, A. Betts for justification.+--+-- @since 0.2.0.0+defaultPreemptionBound :: PreemptionBound+defaultPreemptionBound = 2++-- | A sensible default fair bound: 5.+--+-- This comes from playing around myself, but there is probably a+-- better default.+--+-- @since 0.2.0.0+defaultFairBound :: FairBound+defaultFairBound = 5++-- | A sensible default length bound: 250.+--+-- Based on the assumption that anything which executes for much+-- longer (or even this long) will take ages to test.+--+-- @since 0.2.0.0+defaultLengthBound :: LengthBound+defaultLengthBound = 250
+ Test/DejaFu/Refinement.hs view
@@ -0,0 +1,389 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Test.DejaFu.Refinement+-- Copyright : (c) 2017 Michael Walker+-- License : MIT+-- Maintainer : Michael Walker <mike@barrucadu.co.uk>+-- Stability : experimental+-- Portability : FlexibleContexts, FlexibleInstances, GADTs, TupleSections, TypeFamilies+--+-- Properties about the side-effects of concurrent functions on some+-- shared state.+--+-- Consider this statement about @MVar@s: \"using @readMVar@ is better+-- than @takeMVar@ followed by @putMVar@ because the former is atomic+-- but the latter is not.\"+--+-- This module can test properties like that:+--+-- @+-- sig e = Sig+-- { initialise = maybe newEmptyMVar newMVar+-- , observe = \\v _ -> tryReadMVar v+-- , interfere = \\v s -> tryTakeMVar v >> maybe (pure ()) (void . tryPutMVar v) s+-- , expression = e+-- }+--+-- > check $ sig (void . readMVar) \`equivalentTo\` sig (\\v -> takeMVar v >>= putMVar v)+-- *** Failure: (seed Just ())+-- left: [(Nothing,Just ())]+-- right: [(Nothing,Just ()),(Just Deadlock,Just ())]+-- @+--+-- The two expressions are not equivalent, and we get given the+-- counterexample!+--+-- There are quite a few things going on here, so let's unpack this:+--+-- (1) Properties are specified in terms of an __initialisation__+-- function, an __observation__ function, an __interference__+-- function, and the expression of interest.+--+-- (2) The initialisation function ('initialise') says how to+-- construct some __state__ value from a __seed__ value, which is+-- supplied by 'check'. In this case the seed is of type @Maybe+-- a@ and the state @MVar ConcIO a@.+--+-- (3) The observation ('observe') function says how to take the state+-- and the seed, and produce some value which will be used to+-- compare the expressions. In this case the observation value is+-- of type @Maybe a@.+--+-- (4) The interference ('interfere') function says what sort of+-- concurrent interference can happen. In this case we just try+-- to set the @MVar@ to its original value.+--+-- The 'check' function takes a property, consisting of two signatures+-- and a way to compare them, evaluates all the results of each+-- signature, and then compares them in the appropriate way.+--+-- See the sections later in the documentation for what+-- \"refinement\", \"strict refinement\", and \"equivalence\" mean+-- exactly.+module Test.DejaFu.Refinement+ ( -- * Defining properties+ Sig(..)+ , RefinementProperty+ , expectFailure++ -- ** A refines B++ -- | Refinement (or \"weak refinement\") means that all of the+ -- results of the left are also results of the right. If you think+ -- in terms of sets of results, refinement is subset.+ , refines, (=>=)++ -- ** A strictly refines B++ -- | Strict refinement means that the left refines the right, but+ -- the right does not refine the left. If you think in terms of+ -- sets of results, strict refinement is proper subset.+ , strictlyRefines, (->-)++ -- ** A is equivalent to B++ -- | Equivalence means that the left and right refine each other.+ -- If you think in terms of sets of results, equivalence is+ -- equality.+ , equivalentTo, (===)++ -- * Testing properties+ , FailedProperty(..)+ , Testable(O,X)+ , check+ , check'+ , checkFor+ , counterExamples++ -- * Re-exports+ , Listable(..)+ ) where++import Control.Arrow (first)+import Control.Monad (void)+import Control.Monad.Conc.Class (readMVar, spawn)+import Data.Maybe (isNothing, listToMaybe)+import Data.Set (Set)+import qualified Data.Set as S+import Test.LeanCheck (Listable(..), concatMapT, mapT)++import Test.DejaFu.Conc (ConcIO, Failure, subconcurrency)+import Test.DejaFu.Defaults (defaultMemType, defaultWay)+import Test.DejaFu.SCT (runSCT)++-------------------------------------------------------------------------------+-- Specifying properties++-- | What to check.+data How = Weak | Equiv | Strict deriving Eq++-- | A property which can be given to 'check'.+--+-- @since 0.7.0.0+data RefinementProperty o x where+ RP :: Ord o => How -> Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+ Neg :: RefinementProperty o x -> RefinementProperty o x++-- | A concurrent function and some information about how to execute+-- it and observe its effect.+--+-- * @s@ is the state type (@MVar ConcIO a@ in the example)+-- * @o@ is the observation type (@Maybe a@ in the example)+-- * @x@ is the seed type (@Maybe a@ in the example)+--+-- @since 0.7.0.0+data Sig s o x = Sig+ { initialise :: x -> ConcIO s+ -- ^ Create a new instance of the state variable.+ , observe :: s -> x -> ConcIO o+ -- ^ The observation to make.+ , interfere :: s -> x -> ConcIO ()+ -- ^ Set the state value. This doesn't need to be atomic, or even+ -- guaranteed to work, its purpose is to cause interference.+ , expression :: s -> ConcIO ()+ -- ^ The expression to evaluate.+ }++-- | Indicates that the property is supposed to fail.+expectFailure :: RefinementProperty o x -> RefinementProperty o x+expectFailure = Neg++-- | Observational refinement.+--+-- True iff the result-set of the left expression is a subset (not+-- necessarily proper) of the result-set of the right expression.+--+-- The two signatures can have different state types, this lets you+-- compare the behaviour of different data structures. The+-- observation and seed types must match, however.+--+-- @since 0.7.0.0+refines :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+refines = RP Weak++-- | Infix synonym for 'refines'.+--+-- You might think this should be '=<=', so it looks kind of like a+-- funny subset operator, with @A =<= B@ meaning \"the result-set of A+-- is a subset of the result-set of B\". Unfortunately you would be+-- wrong. The operator used in the literature for refinement has the+-- open end pointing at the LESS general term and the closed end at+-- the MORE general term. It is read as \"is refined by\", not+-- \"refines\". So for consistency with the literature, the open end+-- of @=>=@ points at the less general term, and the closed end at the+-- more general term, to give the same argument order as 'refines'.+--+-- @since 0.7.0.0+(=>=) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+(=>=) = refines++-- | Observational equivalence.+--+-- True iff the result-set of the left expression is equal to the+-- result-set of the right expression.+--+-- The two signatures can have different state types, this lets you+-- compare the behaviour of different data structures. The+-- observation and seed types must match, however.+--+-- @since 0.7.0.0+equivalentTo :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+equivalentTo = RP Equiv++-- | Infix synonym for 'equivalentTo'.+--+-- @since 0.7.0.0+(===) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+(===) = equivalentTo++-- | Strict observational refinement.+--+-- True iff the result-set of the left expression is a proper subset+-- of the result-set of the right expression.+--+-- The two signatures can have different state types, this lets you+-- compare the behaviour of different data structures. The+-- observation and seed types must match, however.+--+-- @since 0.7.0.0+strictlyRefines :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+strictlyRefines = RP Strict++-- | Infix synonym for 'strictlyRefines'+--+-- @since 0.7.0.0+(->-) :: Ord o => Sig s1 o x -> Sig s2 o x -> RefinementProperty o x+(->-) = strictlyRefines+++-------------------------------------------------------------------------------+-- Property testing++-- | Things which can be tested.+--+-- @since 0.7.0.0+class Testable a where+ -- | The observation value type. This is used to compare the+ -- results.+ type O a :: *++ -- | The seed value type. This is used to construct the concurrent+ -- states.+ type X a :: *++ rpropTiers :: a -> [[([String], RefinementProperty (O a) (X a))]]++instance Testable (RefinementProperty o x) where+ type O (RefinementProperty o x) = o+ type X (RefinementProperty o x) = x++ rpropTiers p = [[([], p)]]++instance (Listable a, Show a, Testable b) => Testable (a -> b) where+ type O (a -> b) = O b+ type X (a -> b) = X b++ rpropTiers p = concatMapT resultiersFor tiers where+ resultiersFor x = first (show x:) `mapT` rpropTiers (p x)++-- | A counter example is a seed value and a list of variable+-- assignments.+--+-- @since 0.7.0.0+data FailedProperty o x+ = CounterExample+ { failingSeed :: x+ -- ^ The seed for this set of executions.+ , failingArgs :: [String]+ -- ^ The values of free variables, as strings.+ , leftResults :: Set (Maybe Failure, o)+ -- ^ The set of results of the left signature.+ , rightResults :: Set (Maybe Failure, o)+ -- ^ The set of results of the right signature.+ }+ | NoExpectedFailure+ deriving Show++-- | Check a refinement property with a variety of seed values and+-- variable assignments.+--+-- @since 0.7.0.0+check :: (Testable p, Listable (X p), Eq (X p), Show (X p), Show (O p))+ => p+ -- ^ The property to check.+ -> IO Bool+check p = do+ ce <- check' p+ putStrLn $ case ce of+ Just NoExpectedFailure -> "*** Failure: passed, but expected failure."+ Just c -> init $ unlines+ [ "*** Failure: " +++ (if null (failingArgs c) then "" else unwords (failingArgs c) ++ " ") +++ "(seed " ++ show (failingSeed c) ++ ")"+ , " left: " ++ show (S.toList $ leftResults c)+ , " right: " ++ show (S.toList $ rightResults c)+ ]+ Nothing -> "+++ OK"+ pure (isNothing ce)++-- | A version of 'check' that doesn't print, and returns the+-- counterexample.+--+-- @since 0.7.0.0+check' :: (Testable p, Listable (X p), Eq (X p), Show (X p), Show (O p))+ => p+ -- ^ The property to check.+ -> IO (Maybe (FailedProperty (O p) (X p)))+check' = checkFor 10 100++-- | Like 'check', but take a number of cases to try, also returns the+-- counter example found rather than printing it.+--+-- @since 0.7.0.0+checkFor :: (Testable p, Listable (X p), Eq (X p), Show (X p))+ => Int+ -- ^ Number of seed values per variable-assignment.+ -> Int+ -- ^ Number of variable assignments.+ -> p+ -- ^ The property to check.+ -> IO (Maybe (FailedProperty (O p) (X p)))+checkFor sn vn p = listToMaybe <$> counterExamples sn vn p++-- | Find all counterexamples up to a limit.+--+-- @since 0.7.0.0+counterExamples :: (Testable p, Listable (X p), Eq (X p))+ => Int+ -- ^ Number of seed values per variable-assignment.+ -> Int+ -- ^ Number of variable assignments+ -> p+ -- ^ The property to check.+ -> IO [FailedProperty (O p) (X p)]+counterExamples sn vn p = do+ let seeds = take sn $ concat tiers+ let cases = take vn $ concat (rpropTiers p)+ rs <- mapM (\(vs, p') -> (vs,) <$> checkWithSeeds seeds p') cases+ pure [ cf vs | (vs, Just cf) <- rs ]+++-------------------------------------------------------------------------------+-- Internal++-- | Check a refinement property with given seed values. Returns the+-- counterexample if the property is false.+checkWithSeeds+ :: [x]+ -- ^ Seed values to use.+ -> RefinementProperty o x+ -- ^ The property to check.+ -> IO (Maybe ([String] -> FailedProperty o x))+checkWithSeeds seeds (RP how l r) = do+ ls <- mapM (\x -> (x,) <$> evalSigWithSeed l x) seeds+ rs <- mapM (evalSigWithSeed r) seeds+ let resultsf f = zipWith (\(x, l') r' -> (l' `f` r', x, l', r')) ls rs+ pure . listToMaybe $ case how of+ Weak -> ces (resultsf S.isSubsetOf)+ Strict ->+ -- strict fails if (a) any left result-set is not a subset of+ -- the corresponding right result-set, or (b) every left+ -- result-set is equal to the corresponding right result-set+ let equiv = null (ces (resultsf (==)))+ in ces (resultsf S.isSubsetOf) ++ if equiv then ces (resultsf S.isProperSubsetOf) else []+ Equiv -> ces (resultsf (==))+ where+ ces results = [toCE x lrs rrs | (False, x, lrs, rrs) <- results]++ toCE x lrs rrs args = CounterExample+ { failingSeed = x+ , failingArgs = args+ , leftResults = lrs+ , rightResults = rrs+ }+checkWithSeeds seeds (Neg rp) = do+ r <- checkWithSeeds seeds rp+ pure $ case r of+ Just _ -> Nothing+ Nothing -> Just (const NoExpectedFailure)++-- | Evaluate a signature with a given seed value+evalSigWithSeed :: Ord o+ => Sig s o x+ -> x+ -> IO (Set (Maybe Failure, o))+evalSigWithSeed sig x = do+ results <- runSCT defaultWay defaultMemType $ do+ s <- initialise sig x+ r <- subconcurrency $ do+ j <- spawn (interfere sig s x)+ void (expression sig s)+ void (readMVar j)+ o <- observe sig s x+ pure (either Just (const Nothing) r, o)+ pure . S.fromList $ map (\(Right a, _) -> a) results
Test/DejaFu/SCT.hs view
@@ -12,7 +12,11 @@ -- Systematic testing for concurrent computations. module Test.DejaFu.SCT ( -- * Running Concurrent Programs- Way(..)+ Way+ , systematically+ , randomly+ , uniformly+ , swarmy , runSCT , runSCT' , resultsSet@@ -58,7 +62,6 @@ -- See the BPOR paper for more details. , PreemptionBound(..)- , sctPreBound -- ** Fair Bounding @@ -69,7 +72,6 @@ -- See the BPOR paper for more details. , FairBound(..)- , sctFairBound -- ** Length Bounding @@ -77,7 +79,6 @@ -- terms of primitive actions) of an execution. , LengthBound(..)- , sctLengthBound -- * Random Scheduling @@ -88,7 +89,8 @@ -- because a random scheduler is more chaotic than the real -- scheduler. - , sctRandom+ , sctUniformRandom+ , sctWeightedRandom ) where import Control.DeepSeq (NFData(..))@@ -97,7 +99,7 @@ import qualified Data.Map.Strict as M import Data.Set (Set) import qualified Data.Set as S-import System.Random (RandomGen)+import System.Random (RandomGen, randomR) import Test.DejaFu.Common import Test.DejaFu.Conc@@ -106,27 +108,90 @@ ------------------------------------------------------------------------------- -- Running Concurrent Programs --- | How to explore the possible executions of a concurrent program:------ * Systematically explore all executions within the bounds; or------ * Explore a fixed number of random executions, with the given PRNG.+-- | How to explore the possible executions of a concurrent program. ----- @since 0.6.0.0+-- @since 0.7.0.0 data Way where- Systematically :: Bounds -> Way- Randomly :: RandomGen g => g -> Int -> Way+ Systematic :: Bounds -> Way+ Weighted :: RandomGen g => g -> Int -> Int -> Way+ Uniform :: RandomGen g => g -> Int -> Way instance Show Way where- show (Systematically bs) = "Systematically (" ++ show bs ++ ")"- show (Randomly _ n) = "Randomly <gen> " ++ show n+ show (Systematic bs) = "Systematic (" ++ show bs ++ ")"+ show (Weighted _ n t) = "Weighted <gen> " ++ show (n, t)+ show (Uniform _ n) = "Uniform <gen> " ++ show n --- | Explore possible executions of a concurrent program.+-- | Systematically execute a program, trying all distinct executions+-- within the bounds. ----- * If the 'Way' is @Systematically@, 'sctBound' is used.+-- This corresponds to 'sctBound'. ----- * If the 'Way' is @Randomly@, 'sctRandom' is used.+-- @since 0.7.0.0+systematically+ :: Bounds+ -- ^ The bounds to constrain the exploration.+ -> Way+systematically = Systematic++-- | Randomly execute a program, exploring a fixed number of+-- executions. --+-- Threads are scheduled by a weighted random selection, where weights+-- are assigned randomly on thread creation.+--+-- This corresponds to 'sctWeightedRandom' with weight re-use+-- disabled, and is not guaranteed to find all distinct results+-- (unlike 'systematically' / 'sctBound').+--+-- @since 0.7.0.0+randomly :: RandomGen g+ => g+ -- ^ The random generator to drive the scheduling.+ -> Int+ -- ^ The number of executions to try.+ -> Way+randomly g lim = swarmy g lim 1++-- | Randomly execute a program, exploring a fixed number of+-- executions.+--+-- Threads are scheduled by a uniform random selection.+--+-- This corresponds to 'sctUniformRandom', and is not guaranteed to+-- find all distinct results (unlike 'systematically' / 'sctBound').+--+-- @since 0.7.0.0+uniformly :: RandomGen g+ => g+ -- ^ The random generator to drive the scheduling.+ -> Int+ -- ^ The number of executions to try.+ -> Way+uniformly = Uniform++-- | Randomly execute a program, exploring a fixed number of+-- executions.+--+-- Threads are scheduled by a weighted random selection, where weights+-- are assigned randomly on thread creation.+--+-- This corresponds to 'sctWeightedRandom', and is not guaranteed to+-- find all distinct results (unlike 'systematically' / 'sctBound').+--+-- @since 0.7.0.0+swarmy :: RandomGen g+ => g+ -- ^ The random generator to drive the scheduling.+ -> Int+ -- ^ The number of executions to try.+ -> Int+ -- ^ The number of executions to use the thread weights for.+ -> Way+swarmy = Weighted++-- | Explore possible executions of a concurrent program according to+-- the given 'Way'.+-- -- @since 0.6.0.0 runSCT :: MonadRef r n => Way@@ -136,8 +201,9 @@ -> ConcT r n a -- ^ The computation to run many times. -> n [(Either Failure a, Trace)]-runSCT (Systematically cb) memtype = sctBound memtype cb-runSCT (Randomly g lim) memtype = sctRandom memtype g lim+runSCT (Systematic cb) memtype = sctBound memtype cb+runSCT (Weighted g lim use) memtype = sctWeightedRandom memtype g lim use+runSCT (Uniform g lim) memtype = sctUniformRandom memtype g lim -- | A strict variant of 'runSCT'. --@@ -235,20 +301,6 @@ -- not derived, so it can have a separate @since annotation rnf (PreemptionBound i) = rnf i --- | An SCT runner using a pre-emption bounding scheduler.------ @since 0.4.0.0-sctPreBound :: MonadRef r n- => MemType- -- ^ The memory model to use for non-synchronised @CRef@ operations.- -> PreemptionBound- -- ^ The maximum number of pre-emptions to allow in a single- -- execution- -> ConcT r n a- -- ^ The computation to run many times- -> n [(Either Failure a, Trace)]-sctPreBound memtype pb = sctBound memtype $ Bounds (Just pb) Nothing Nothing- -- | Pre-emption bound function. This does not count pre-emptive -- context switches to a commit thread. pBound :: PreemptionBound -> BoundFunc@@ -286,20 +338,6 @@ -- not derived, so it can have a separate @since annotation rnf (FairBound i) = rnf i --- | An SCT runner using a fair bounding scheduler.------ @since 0.4.0.0-sctFairBound :: MonadRef r n- => MemType- -- ^ The memory model to use for non-synchronised @CRef@ operations.- -> FairBound- -- ^ The maximum difference between the number of yield operations- -- performed by different threads.- -> ConcT r n a- -- ^ The computation to run many times- -> n [(Either Failure a, Trace)]-sctFairBound memtype fb = sctBound memtype $ Bounds Nothing (Just fb) Nothing- -- | Fair bound function fBound :: FairBound -> BoundFunc fBound (FairBound fb) ts (_, l) = maxYieldCountDiff ts l <= fb@@ -323,20 +361,6 @@ -- not derived, so it can have a separate @since annotation rnf (LengthBound i) = rnf i --- | An SCT runner using a length bounding scheduler.------ @since 0.4.0.0-sctLengthBound :: MonadRef r n- => MemType- -- ^ The memory model to use for non-synchronised @CRef@ operations.- -> LengthBound- -- ^ The maximum length of a schedule, in terms of primitive- -- actions.- -> ConcT r n a- -- ^ The computation to run many times- -> n [(Either Failure a, Trace)]-sctLengthBound memtype lb = sctBound memtype $ Bounds Nothing Nothing (Just lb)- -- | Length bound function lBound :: LengthBound -> BoundFunc lBound (LengthBound lb) ts _ = length ts < lb@@ -401,15 +425,15 @@ -- Incorporate the new backtracking steps into the DPOR tree. addBacktracks = incorporateBacktrackSteps (cBound cb) --- | SCT via random scheduling.+-- | SCT via uniform random scheduling. -- -- Schedules are generated by assigning to each new thread a random -- weight. Threads are then scheduled by a weighted random selection. -- -- This is not guaranteed to find all distinct results. ----- @since 0.5.0.0-sctRandom :: (MonadRef r n, RandomGen g)+-- @since 0.7.0.0+sctUniformRandom :: (MonadRef r n, RandomGen g) => MemType -- ^ The memory model to use for non-synchronised @CRef@ operations. -> g@@ -419,15 +443,44 @@ -> ConcT r n a -- ^ The computation to run many times. -> n [(Either Failure a, Trace)]-sctRandom memtype g0 lim0 conc = go g0 lim0 where+sctUniformRandom memtype g0 lim0 conc = go g0 (max 0 lim0) where go _ 0 = pure [] go g n = do- (res, s, trace) <- runConcurrent randSched+ (res, s, trace) <- runConcurrent (randSched $ \g' -> (1, g')) memtype- (initialRandSchedState g)+ (initialRandSchedState Nothing g) conc- ((res, trace):) <$> go (schedGen s) (n-1)++-- | SCT via weighted random scheduling.+--+-- Schedules are generated by assigning to each new thread a random+-- weight. Threads are then scheduled by a weighted random selection.+--+-- This is not guaranteed to find all distinct results.+--+-- @since 0.7.0.0+sctWeightedRandom :: (MonadRef r n, RandomGen g)+ => MemType+ -- ^ The memory model to use for non-synchronised @CRef@ operations.+ -> g+ -- ^ The random number generator.+ -> Int+ -- ^ The number of executions to perform.+ -> Int+ -- ^ The number of executions to use the same set of weights for.+ -> ConcT r n a+ -- ^ The computation to run many times.+ -> n [(Either Failure a, Trace)]+sctWeightedRandom memtype g0 lim0 use0 conc = go g0 (max 0 lim0) (max 1 use0) M.empty where+ go _ 0 _ _ = pure []+ go g n 0 _ = go g n (max 1 use0) M.empty+ go g n use ws = do+ (res, s, trace) <- runConcurrent (randSched $ randomR (1, 50))+ memtype+ (initialRandSchedState (Just ws) g)+ conc+ ((res, trace):) <$> go (schedGen s) (n-1) (use-1) (schedWeights s) ------------------------------------------------------------------------------- -- Utilities
Test/DejaFu/SCT/Internal.hs view
@@ -13,15 +13,14 @@ import Control.DeepSeq (NFData(..)) import Control.Exception (MaskingState(..))-import Data.Char (ord) import qualified Data.Foldable as F import Data.Function (on)-import Data.List (intercalate, nubBy, partition, sortOn)+import Data.List (nubBy, partition, sortOn) import Data.List.NonEmpty (NonEmpty(..), toList) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M-import Data.Maybe (catMaybes, fromJust, isJust, isNothing,- listToMaybe)+import Data.Maybe (catMaybes, fromJust, fromMaybe, isJust,+ isNothing, listToMaybe) import Data.Sequence (Seq, (|>)) import qualified Data.Sequence as Sq import Data.Set (Set)@@ -531,14 +530,14 @@ ) -- | Initial weighted random scheduler state.-initialRandSchedState :: g -> RandSchedState g-initialRandSchedState = RandSchedState M.empty+initialRandSchedState :: Maybe (Map ThreadId Int) -> g -> RandSchedState g+initialRandSchedState = RandSchedState . fromMaybe M.empty -- | Weighted random scheduler: assigns to each new thread a weight, -- and makes a weighted random choice out of the runnable threads at -- every step.-randSched :: RandomGen g => Scheduler (RandSchedState g)-randSched _ _ threads s = (pick choice enabled, RandSchedState weights' g'') where+randSched :: RandomGen g => (g -> (Int, g)) -> Scheduler (RandSchedState g)+randSched weightf _ _ threads s = (pick choice enabled, RandSchedState weights' g'') where -- Select a thread pick idx ((x, f):xs) | idx < f = Just x@@ -551,7 +550,7 @@ weights' = schedWeights s `M.union` M.fromList newWeights (newWeights, g') = foldr assignWeight ([], schedGen s) $ filter (`M.notMember` schedWeights s) tids assignWeight tid ~(ws, g0) =- let (w, g) = randomR (1, 50) g0+ let (w, g) = weightf g0 in ((tid, w):ws, g) -- The runnable threads.
dejafu.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: dejafu-version: 0.6.0.0+version: 0.7.0.0 synopsis: Systematic testing for Haskell concurrency. description:@@ -37,12 +37,14 @@ source-repository this type: git location: https://github.com/barrucadu/dejafu.git- tag: dejafu-0.6.0.0+ tag: dejafu-0.7.0.0 library exposed-modules: Test.DejaFu , Test.DejaFu.Conc , Test.DejaFu.Common+ , Test.DejaFu.Defaults+ , Test.DejaFu.Refinement , Test.DejaFu.Schedule , Test.DejaFu.SCT , Test.DejaFu.STM@@ -61,6 +63,7 @@ , containers >=0.5 && <0.6 , deepseq >=1.1 && <2 , exceptions >=0.7 && <0.9+ , leancheck >=0.6 && <0.7 , monad-loops >=0.4 && <0.5 , mtl >=2.2 && <2.3 , random >=1.0 && <1.2