Skip to content

Commit e1fe79d

Browse files
Initial stab at parallel actions
1 parent 252826f commit e1fe79d

File tree

11 files changed

+617
-152
lines changed

11 files changed

+617
-152
lines changed

quickcheck-dynamic/quickcheck-dynamic.cabal

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ source-repository head
2727
type: git
2828
location: https://github.com/input-output-hk/quickcheck-dynamic
2929

30+
flag dev
31+
default: False
32+
3033
common lang
3134
default-language: Haskell2010
3235
default-extensions:
@@ -62,6 +65,7 @@ common lang
6265
-Wall -Wnoncanonical-monad-instances -Wunused-packages
6366
-Wincomplete-uni-patterns -Wincomplete-record-updates
6467
-Wredundant-constraints -Widentities -Wno-unused-do-bind
68+
-Wno-name-shadowing -Wno-x-partial
6569

6670
library
6771
import: lang
@@ -76,6 +80,23 @@ library
7680
Test.QuickCheck.Extras
7781
Test.QuickCheck.StateModel
7882
Test.QuickCheck.StateModel.Variables
83+
Test.QuickCheck.ParallelActions
84+
85+
if flag(dev)
86+
hs-source-dirs: test
87+
exposed-modules:
88+
Spec.DynamicLogic.Counters
89+
Spec.DynamicLogic.Registry
90+
Spec.DynamicLogic.RegistryModel
91+
Test.QuickCheck.DynamicLogic.QuantifySpec
92+
Test.QuickCheck.StateModelSpec
93+
build-depends:
94+
, io-classes
95+
, io-sim
96+
, stm
97+
, tasty
98+
, tasty-hunit
99+
, tasty-quickcheck
79100

80101
build-depends:
81102
, base >=4.7 && <5
@@ -100,6 +121,8 @@ test-suite quickcheck-dynamic-test
100121
build-depends:
101122
, base
102123
, containers
124+
, io-classes
125+
, io-sim
103126
, mtl
104127
, QuickCheck
105128
, quickcheck-dynamic

quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ instance Monad (DL s) where
6060
instance MonadFail (DL s) where
6161
fail = errorDL
6262

63-
action :: (Typeable a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s (Var a)
63+
action :: (Typeable a, Show a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s (Var a)
6464
action cmd = DL $ \_ k -> DL.after cmd k
6565

66-
failingAction :: (Typeable a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s ()
66+
failingAction :: (Typeable a, Show a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s ()
6767
failingAction cmd = DL $ \_ k -> DL.afterNegative cmd (k ())
6868

6969
anyAction :: DL s ()
@@ -96,7 +96,7 @@ getModelStateDL = DL $ \s k -> k (underlyingState s) s
9696
getVarContextDL :: DL s VarContext
9797
getVarContextDL = DL $ \s k -> k (vars s) s
9898

99-
forAllVar :: forall a s. Typeable a => DL s (Var a)
99+
forAllVar :: forall a s. (Typeable a, Show a) => DL s (Var a)
100100
forAllVar = do
101101
xs <- ctxAtType <$> getVarContextDL
102102
forAllQ $ elementsQ xs

quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Internal.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ data DynLogic s
3333
Stopping (DynLogic s)
3434
| -- | After a specific action the predicate should hold
3535
forall a.
36-
(Eq (Action s a), Show (Action s a), Typeable a) =>
36+
(Eq (Action s a), Show (Action s a), Typeable a, Show a) =>
3737
After (ActionWithPolarity s a) (Var a -> DynPred s)
3838
| Error String (DynPred s)
3939
| -- | Adjust the probability of picking a branch
@@ -66,7 +66,7 @@ afterAny :: (Annotated s -> DynFormula s) -> DynFormula s
6666
afterAny f = DynFormula $ \n -> AfterAny $ \s -> unDynFormula (f s) n
6767

6868
afterPolar
69-
:: (Typeable a, Eq (Action s a), Show (Action s a))
69+
:: (Typeable a, Show a, Eq (Action s a), Show (Action s a))
7070
=> ActionWithPolarity s a
7171
-> (Var a -> Annotated s -> DynFormula s)
7272
-> DynFormula s
@@ -75,7 +75,7 @@ afterPolar act f = DynFormula $ \n -> After act $ \x s -> unDynFormula (f x s) n
7575
-- | Given `f` must be `True` after /some/ action.
7676
-- `f` is passed the state resulting from executing the `Action`.
7777
after
78-
:: (Typeable a, Eq (Action s a), Show (Action s a))
78+
:: (Typeable a, Show a, Eq (Action s a), Show (Action s a))
7979
=> Action s a
8080
-> (Var a -> Annotated s -> DynFormula s)
8181
-> DynFormula s
@@ -85,7 +85,7 @@ after act f = afterPolar (ActionWithPolarity act PosPolarity) f
8585
-- `f` is passed the state resulting from executing the `Action`
8686
-- as a negative action.
8787
afterNegative
88-
:: (Typeable a, Eq (Action s a), Show (Action s a))
88+
:: (Typeable a, Show a, Eq (Action s a), Show (Action s a))
8989
=> Action s a
9090
-> (Annotated s -> DynFormula s)
9191
-> DynFormula s

quickcheck-dynamic/src/Test/QuickCheck/Extras.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Test.QuickCheck.Extras where
22

33
import Control.Monad.Reader
44
import Control.Monad.State
5+
import Test.QuickCheck
56
import Test.QuickCheck.Monadic
67

78
runPropertyStateT :: Monad m => PropertyM (StateT s m) a -> s -> PropertyM m (a, s)
@@ -13,3 +14,9 @@ runPropertyReaderT :: Monad m => PropertyM (ReaderT e m) a -> e -> PropertyM m a
1314
runPropertyReaderT p e = MkPropertyM $ \k -> do
1415
m <- unPropertyM p $ fmap lift . k
1516
return $ runReaderT m e
17+
18+
sometimes :: Testable p => Int -> p -> Property
19+
sometimes i = disjoin . replicate i
20+
21+
always :: Testable p => Int -> p -> Property
22+
always i = conjoin . replicate i
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE AllowAmbiguousTypes #-}
3+
-- | Experimental support for running parallel actions
4+
module Test.QuickCheck.ParallelActions
5+
( RunModelPar(..)
6+
, Forking(..)
7+
, ParallelActions
8+
, runParActions
9+
) where
10+
11+
import Data.Set qualified as Set
12+
import Control.Monad
13+
import Control.Monad.Reader
14+
import Control.Monad.Writer
15+
import Data.Data
16+
import GHC.Generics
17+
import Test.QuickCheck hiding (Some)
18+
import Test.QuickCheck.Monadic
19+
import Test.QuickCheck.StateModel.Variables
20+
import Test.QuickCheck.StateModel
21+
import Control.Concurrent
22+
import Control.Arrow (first, second)
23+
import Data.Tree
24+
25+
-- | The callbacks necessary to run actions in parallel
26+
class RunModel state m => RunModelPar state m where
27+
-- | Note that this version of `perform` doesn't get the current `state`.
28+
-- This is because the system is not in a definite model state during
29+
-- parallel execution.
30+
performPar :: Typeable a => Action state a -> LookUp -> m (PerformResult state m a)
31+
performPar = perform (error "Trying to evaluate state in default implementation of performPar")
32+
33+
-- | Like `monitoring` but without the `state`
34+
monitoringPar :: Action state a -> LookUp -> Either (Error state m) a -> Property -> Property
35+
monitoringPar _ _ _ = id
36+
37+
data ParallelActions state =
38+
ParallelActions { linearActions :: Actions state
39+
, threads :: [[Int]]
40+
} deriving (Eq, Generic)
41+
42+
commonActions :: ParallelActions state -> [Step state]
43+
commonActions ParallelActions{linearActions = Actions steps, ..} =
44+
[ step | step@(v := _) <- steps, notElem (unsafeVarIndex v) $ concat threads ]
45+
46+
threadActions :: ParallelActions state -> [[Step state]]
47+
threadActions ParallelActions{linearActions = Actions steps, ..} =
48+
[ [ v := s{polarity = PosPolarity}
49+
| v := s <- steps, elem (unsafeVarIndex v) thread ]
50+
| thread <- threads
51+
]
52+
53+
firstParAction :: ParallelActions state -> Maybe Int
54+
firstParAction ParallelActions{..}
55+
| null idxs = Nothing
56+
| otherwise = Just $ minimum idxs
57+
where idxs = concat threads
58+
59+
instance StateModel state => Show (ParallelActions state) where
60+
show pas =
61+
unlines $ [ "-- Common Prefix:"
62+
, showWithUsed (foldMap allVariables threads) common
63+
] ++ concat [ [ "-- Thread " ++ [n]
64+
, show thread
65+
]
66+
| (n, thread) <- zip ['A'..'Z'] threads
67+
]
68+
where
69+
common = Actions $ commonActions pas
70+
threads = Actions <$> threadActions pas
71+
72+
instance StateModel state => Arbitrary (ParallelActions state) where
73+
arbitrary = genParActions
74+
75+
shrink pas@(ParallelActions actions trs) =
76+
[ ParallelActions actions $ map (filter (/= i)) trs | Just i <- [firstParAction pas]] ++
77+
filter checkParallelActions
78+
[ ParallelActions actions $ filter (not . null) $ map (filter (`Set.member` vars)) trs
79+
| actions <- shrink actions
80+
, let vars = unsafeIndexSet $ allVariables actions
81+
]
82+
83+
checkParallelActions :: StateModel state => ParallelActions state -> Bool
84+
checkParallelActions pas = all (checkWellTypedness commonCtx) (threadActions pas)
85+
where
86+
commonCtx = allVariables common
87+
common = Actions $ commonActions pas
88+
89+
checkWellTypedness :: StateModel state => VarContext -> [Step state] -> Bool
90+
checkWellTypedness _ [] = True
91+
checkWellTypedness ctx ((v := a) : ss) = a `wellTypedIn` ctx && checkWellTypedness (extendContext ctx v) ss
92+
93+
genParActions :: forall state. StateModel state => Gen (ParallelActions state)
94+
genParActions = do
95+
-- The ~ works around a bug in ghc (https://gitlab.haskell.org/ghc/ghc/-/issues/22004) (which is not in all ghc versions ?!)
96+
as@(~(Actions steps)) <- arbitrary
97+
let n = length steps
98+
split <- choose (max 0 (n - 20), n - 1)
99+
let (common, post) = splitAt split steps
100+
commonCtx = allVariables common
101+
tc <- choose (2, 5)
102+
threads <- go post $ replicate tc (commonCtx, [])
103+
return $ ParallelActions as $ filter (not . null) threads
104+
where go :: [Step state] -> [(VarContext, [Int])] -> Gen [[Int]]
105+
go [] trs = return $ map (reverse . snd) trs
106+
go ((v := a) : ss) trs = do
107+
let candidates = [ (ctx, tr, trs) | ((ctx, tr), trs) <- holes trs
108+
, a `wellTypedIn` ctx ]
109+
if null candidates
110+
-- This means we made a mistake earlier and split two actions whose
111+
-- result variables were used together later. At this point we just
112+
-- give up and don't extend the traces.
113+
then go [] trs
114+
else do
115+
(ctx, tr, trs) <- elements candidates
116+
go ss $ (extendContext ctx v, unsafeVarIndex v:tr) : trs
117+
118+
data TraceStep state m where
119+
TraceStep :: (Typeable a, Show a)
120+
=> Either (Error state m) a
121+
-> Var a
122+
-> ActionWithPolarity state a
123+
-> TraceStep state m
124+
125+
type Trace state m = [TraceStep state m]
126+
type TraceTree state m = Tree (TraceStep state m)
127+
128+
runTracing :: ( RunModelPar state m
129+
, e ~ Error state m
130+
, forall a. IsPerformResult e a
131+
) => Env -> [Step state] -> m (Trace state m, Env)
132+
runTracing env [] = return ([], env)
133+
runTracing env ((v := ap):as) = do
134+
r <- performResultToEither <$> performPar (polarAction ap) (lookUpVar env)
135+
let step = TraceStep r v ap
136+
env' | Right val <- r = (v :== val) : env
137+
| otherwise = env
138+
(first (step :)) <$> runTracing env' as
139+
140+
class Monad m => Forking m where
141+
forkThread :: m a -> m (m a)
142+
143+
instance Forking IO where
144+
forkThread io = do
145+
t <- newEmptyMVar
146+
forkIO $ io >>= putMVar t
147+
return $ takeMVar t
148+
149+
instance Forking m => Forking (ReaderT r m) where
150+
forkThread m = do
151+
reg <- ask
152+
lift $ fmap lift (forkThread $ runReaderT m reg)
153+
154+
-- | Run parallel actions consisting of a common prefix and a number of
155+
-- parallel threads. After execution check that the preconditions were
156+
-- respected in all possible parallel executions and check that we find at
157+
-- least one parallel execution which is linearizible.
158+
runParActions :: ( StateModel state
159+
, RunModelPar state m
160+
, e ~ Error state m
161+
, forall a. IsPerformResult e a
162+
, Forking m
163+
) => ParallelActions state -> PropertyM m ()
164+
runParActions pas = do
165+
(trC, env) <- run $ runTracing mempty $ commonActions pas
166+
joins <- mapM (run . forkThread . runTracing env) (threadActions pas)
167+
trs <- mapM (fmap fst . run) joins
168+
let used = varsUsedInActions $ linearActions pas
169+
monitor $ counterexample "-- Main thread:"
170+
monitorTrace used mempty trC
171+
forM (zip ['A'..'Z'] trs) $ \ (n, tr) -> do
172+
monitor $ counterexample $ "\n-- Thread " ++ [n, ':']
173+
monitorTrace used env tr
174+
let ilvs = prepend trC $ interleavings trs
175+
monitor $ tabulate "Trace tree size" (map (bucket . length) ilvs)
176+
assert $ null ilvs || any (checkTrace initialAnnotatedState mempty) ilvs
177+
178+
monitorTrace :: forall state m. (StateModel state, RunModelPar state m)
179+
=> VarContext -> Env -> Trace state m -> PropertyM m ()
180+
monitorTrace _used _env [] = pure ()
181+
monitorTrace used env (TraceStep r v act : tr) = do
182+
let showR (Right x)
183+
| v `wellTypedIn` used = show v ++ "@" ++ showsPrec 10 x ""
184+
| otherwise = show x
185+
showR (Left err) = "fail " ++ showsPrec 10 err ""
186+
monitor $ counterexample (showR r ++ " <- " ++ show (polarAction act))
187+
monitor $ monitoringPar @state @m (polarAction act) (lookUpVar env) r
188+
monitorTrace used env' tr
189+
where
190+
env' | Right val <- r = (v :== val) : env
191+
| otherwise = env
192+
193+
checkTrace :: forall state m. (StateModel state, RunModelPar state m)
194+
=> Annotated state -> Env -> TraceTree state m -> Bool
195+
checkTrace s env (Node (TraceStep r v (ActionWithPolarity a _)) trs) =
196+
-- NOTE: we need to re-compute the polarity of `a` here because it may be that the failure can be explained,
197+
-- but only by the action failing when it was previous successful
198+
let act = actionWithPolarity s a
199+
s' = computeNextState s act v
200+
env' | Right val <- r = (v :== val) : env
201+
| otherwise = env
202+
checkPost | Right val <- r, polarity act == PosPolarity = fst . runWriter . runPost $ postcondition @state @m
203+
(underlyingState s, underlyingState s')
204+
(polarAction act)
205+
(lookUpVar env')
206+
val
207+
| Left{} <- r, polarity act == PosPolarity = False
208+
| otherwise = fst . runWriter . runPost $ postconditionOnFailure @state @m
209+
(underlyingState s, underlyingState s')
210+
(polarAction act)
211+
(lookUpVar env')
212+
r
213+
in (computePrecondition s act || discard) && checkPost && (null trs || any (checkTrace s' env') trs)
214+
215+
prepend :: [a] -> [Tree a] -> [Tree a]
216+
prepend [] ts = ts
217+
prepend (p:ps) ts = [Node p $ prepend ps ts]
218+
219+
interleavings :: [[a]] -> [Tree a]
220+
interleavings aas = do
221+
(a:as, os) <- holes aas
222+
pure $ Node a (interleavings (as:os))
223+
224+
holes :: [a] -> [(a, [a])]
225+
holes [] = []
226+
holes (a:as) = (a, as) : map (second (a:)) (holes as)

0 commit comments

Comments
 (0)