{-# OPTIONS_HADDOCK not-home #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Hedgehog.Internal.State (
Var(..)
, concrete
, opaque
, Concrete(..)
, Symbolic(..)
, Name(..)
, Environment(..)
, EnvironmentError(..)
, emptyEnvironment
, insertConcrete
, reifyDynamic
, reifyEnvironment
, reify
, Command(..)
, Callback(..)
, commandGenOK
, Action(..)
, Sequential(..)
, Parallel(..)
, takeVariables
, variablesOK
, dropInvalid
, action
, sequential
, parallel
, executeSequential
, executeParallel
) where
import qualified Control.Concurrent.Async.Lifted as Async
import Control.Monad (foldM, foldM_)
import Control.Monad.Catch (MonadCatch)
import Control.Monad.State.Class (MonadState, get, put, modify)
import Control.Monad.Morph (MFunctor(..))
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.State (State, runState, execState)
import Control.Monad.Trans.State (StateT(..), evalStateT, runStateT)
import Data.Dynamic (Dynamic, toDyn, fromDynamic, dynTypeRep)
import Data.Foldable (traverse_)
import Data.Functor.Classes (Eq1(..), Ord1(..), Show1(..))
import Data.Functor.Classes (eq1, compare1, showsPrec1)
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Typeable (Typeable, TypeRep, Proxy(..), typeRep)
import Hedgehog.Internal.Distributive (distributeT)
import Hedgehog.Internal.Gen (MonadGen, GenT, GenBase)
import qualified Hedgehog.Internal.Gen as Gen
import Hedgehog.Internal.HTraversable (HTraversable(..))
import Hedgehog.Internal.Opaque (Opaque(..))
import Hedgehog.Internal.Property (MonadTest(..), Test, evalEither, evalM, success, runTest, failWith, annotate)
import Hedgehog.Internal.Range (Range)
import Hedgehog.Internal.Show (showPretty)
import Hedgehog.Internal.Source (HasCallStack, withFrozenCallStack)
newtype Name =
Name Int
deriving (Eq, Ord, Num)
instance Show Name where
showsPrec p (Name x) =
showsPrec p x
data Symbolic a where
Symbolic :: Typeable a => Name -> Symbolic a
deriving instance Eq (Symbolic a)
deriving instance Ord (Symbolic a)
instance Show (Symbolic a) where
showsPrec p (Symbolic x) =
showsPrec p x
instance Show1 Symbolic where
liftShowsPrec _ _ p (Symbolic x) =
showsPrec p x
instance Eq1 Symbolic where
liftEq _ (Symbolic x) (Symbolic y) =
x == y
instance Ord1 Symbolic where
liftCompare _ (Symbolic x) (Symbolic y) =
compare x y
newtype Concrete a where
Concrete :: a -> Concrete a
deriving (Eq, Ord, Functor, Foldable, Traversable)
instance Show a => Show (Concrete a) where
showsPrec =
showsPrec1
instance Show1 Concrete where
liftShowsPrec sp _ p (Concrete x) =
sp p x
instance Eq1 Concrete where
liftEq eq (Concrete x) (Concrete y) =
eq x y
instance Ord1 Concrete where
liftCompare comp (Concrete x) (Concrete y) =
comp x y
newtype Var a v =
Var (v a)
concrete :: Var a Concrete -> a
concrete (Var (Concrete x)) =
x
opaque :: Var (Opaque a) Concrete -> a
opaque (Var (Concrete (Opaque x))) =
x
instance (Eq a, Eq1 v) => Eq (Var a v) where
(==) (Var x) (Var y) =
eq1 x y
instance (Ord a, Ord1 v) => Ord (Var a v) where
compare (Var x) (Var y) =
compare1 x y
instance (Show a, Show1 v) => Show (Var a v) where
showsPrec p (Var x) =
showParen (p >= 11) $
showString "Var " .
showsPrec1 11 x
instance HTraversable (Var a) where
htraverse f (Var v) =
fmap Var (f v)
newtype Environment =
Environment {
unEnvironment :: Map Name Dynamic
} deriving (Show)
data EnvironmentError =
EnvironmentValueNotFound !Name
| EnvironmentTypeError !TypeRep !TypeRep
deriving (Eq, Ord, Show)
emptyEnvironment :: Environment
emptyEnvironment =
Environment Map.empty
unionsEnvironment :: [Environment] -> Environment
unionsEnvironment =
Environment . Map.unions . fmap unEnvironment
insertConcrete :: Symbolic a -> Concrete a -> Environment -> Environment
insertConcrete (Symbolic k) (Concrete v) =
Environment . Map.insert k (toDyn v) . unEnvironment
reifyDynamic :: forall a. Typeable a => Dynamic -> Either EnvironmentError (Concrete a)
reifyDynamic dyn =
case fromDynamic dyn of
Nothing ->
Left $ EnvironmentTypeError (typeRep (Proxy :: Proxy a)) (dynTypeRep dyn)
Just x ->
Right $ Concrete x
reifyEnvironment :: Environment -> (forall a. Symbolic a -> Either EnvironmentError (Concrete a))
reifyEnvironment (Environment vars) (Symbolic n) =
case Map.lookup n vars of
Nothing ->
Left $ EnvironmentValueNotFound n
Just dyn ->
reifyDynamic dyn
reify :: HTraversable t => Environment -> t Symbolic -> Either EnvironmentError (t Concrete)
reify vars =
htraverse (reifyEnvironment vars)
data Callback input output state =
Require (state Symbolic -> input Symbolic -> Bool)
| Update (forall v. Ord1 v => state v -> input v -> Var output v -> state v)
| Ensure (state Concrete -> state Concrete -> input Concrete -> output -> Test ())
callbackRequire1 ::
state Symbolic
-> input Symbolic
-> Callback input output state
-> Bool
callbackRequire1 s i = \case
Require f ->
f s i
Update _ ->
True
Ensure _ ->
True
callbackUpdate1 ::
Ord1 v
=> state v
-> input v
-> Var output v
-> Callback input output state
-> state v
callbackUpdate1 s i o = \case
Require _ ->
s
Update f ->
f s i o
Ensure _ ->
s
callbackEnsure1 ::
state Concrete
-> state Concrete
-> input Concrete
-> output
-> Callback input output state
-> Test ()
callbackEnsure1 s0 s i o = \case
Require _ ->
success
Update _ ->
success
Ensure f ->
f s0 s i o
callbackRequire ::
[Callback input output state]
-> state Symbolic
-> input Symbolic
-> Bool
callbackRequire callbacks s i =
all (callbackRequire1 s i) callbacks
callbackUpdate ::
Ord1 v
=> [Callback input output state]
-> state v
-> input v
-> Var output v
-> state v
callbackUpdate callbacks s0 i o =
foldl (\s -> callbackUpdate1 s i o) s0 callbacks
callbackEnsure ::
[Callback input output state]
-> state Concrete
-> state Concrete
-> input Concrete
-> output
-> Test ()
callbackEnsure callbacks s0 s i o =
traverse_ (callbackEnsure1 s0 s i o) callbacks
data Command gen m (state :: (* -> *) -> *) =
forall input output.
(HTraversable input, Show (input Symbolic), Show output, Typeable output) =>
Command {
commandGen ::
state Symbolic -> Maybe (gen (input Symbolic))
, commandExecute ::
input Concrete -> m output
, commandCallbacks ::
[Callback input output state]
}
commandGenOK :: Command gen m state -> state Symbolic -> Bool
commandGenOK (Command inputGen _ _) state =
Maybe.isJust (inputGen state)
data Action m (state :: (* -> *) -> *) =
forall input output.
(HTraversable input, Show (input Symbolic), Show output) =>
Action {
actionInput ::
input Symbolic
, actionOutput ::
Symbolic output
, actionExecute ::
input Concrete -> m output
, actionRequire ::
state Symbolic -> input Symbolic -> Bool
, actionUpdate ::
forall v. Ord1 v => state v -> input v -> Var output v -> state v
, actionEnsure ::
state Concrete -> state Concrete -> input Concrete -> output -> Test ()
}
instance Show (Action m state) where
showsPrec p (Action input (Symbolic (Name output)) _ _ _ _) =
showParen (p > 10) $
showString "Var " .
showsPrec 11 output .
showString " :<- " .
showsPrec 11 input
takeSymbolic :: forall a. Symbolic a -> (Name, TypeRep)
takeSymbolic (Symbolic name) =
(name, typeRep (Proxy :: Proxy a))
insertSymbolic :: Symbolic a -> Map Name TypeRep -> Map Name TypeRep
insertSymbolic s =
let
(name, typ) =
takeSymbolic s
in
Map.insert name typ
takeVariables :: forall t. HTraversable t => t Symbolic -> Map Name TypeRep
takeVariables xs =
let
go x = do
modify (insertSymbolic x)
pure x
in
flip execState Map.empty $ htraverse go xs
variablesOK :: HTraversable t => t Symbolic -> Map Name TypeRep -> Bool
variablesOK xs allowed =
let
vars =
takeVariables xs
in
Map.null (vars `Map.difference` allowed) &&
and (Map.intersectionWith (==) vars allowed)
data Context state =
Context {
contextState :: state Symbolic
, _contextVars :: Map Name TypeRep
}
mkContext :: state Symbolic -> Context state
mkContext initial =
Context initial Map.empty
contextUpdate :: MonadState (Context state) m => state Symbolic -> m ()
contextUpdate state = do
Context _ vars <- get
put $ Context state vars
contextNewVar :: (MonadState (Context state) m, Typeable a) => m (Symbolic a)
contextNewVar = do
Context state vars <- get
let
var =
case Map.maxViewWithKey vars of
Nothing ->
Symbolic 0
Just ((name, _), _) ->
Symbolic (name + 1)
put $ Context state (insertSymbolic var vars)
pure var
dropInvalid :: [Action m state] -> State (Context state) [Action m state]
dropInvalid =
let
loop step@(Action input output _execute require update _ensure) = do
Context state0 vars0 <- get
if require state0 input && variablesOK input vars0 then do
let
state =
update state0 input (Var output)
vars =
insertSymbolic output vars0
put $ Context state vars
pure $ Just step
else
pure Nothing
in
fmap Maybe.catMaybes . traverse loop
action ::
(MonadGen gen, MonadTest m)
=> [Command gen m state]
-> GenT (StateT (Context state) (GenBase gen)) (Action m state)
action commands =
Gen.justT $ do
Context state0 _ <- get
Command mgenInput exec callbacks <-
Gen.element $ filter (\c -> commandGenOK c state0) commands
input <-
case mgenInput state0 of
Nothing ->
error "genCommand: internal error, tried to use generator with invalid state."
Just gen ->
hoist lift $ Gen.toGenT gen
if not $ callbackRequire callbacks state0 input then
pure Nothing
else do
output <- contextNewVar
contextUpdate $
callbackUpdate callbacks state0 input (Var output)
pure . Just $
Action input output exec
(callbackRequire callbacks)
(callbackUpdate callbacks)
(callbackEnsure callbacks)
genActions ::
(MonadGen gen, MonadTest m)
=> Range Int
-> [Command gen m state]
-> Context state
-> gen ([Action m state], Context state)
genActions range commands ctx = do
xs <- Gen.fromGenT . (`evalStateT` ctx) . distributeT $ Gen.list range (action commands)
pure $
dropInvalid xs `runState` ctx
data Sequential m state =
Sequential {
sequentialActions :: [Action m state]
}
renderAction :: Action m state -> [String]
renderAction (Action input (Symbolic (Name output)) _ _ _ _) =
let
prefix0 =
"Var " ++ show output ++ " = "
prefix =
replicate (length prefix0) ' '
in
case lines (showPretty input) of
[] ->
[prefix0 ++ "?"]
x : xs ->
(prefix0 ++ x) :
fmap (prefix ++) xs
renderActionResult :: Environment -> Action m state -> [String]
renderActionResult env (Action _ output@(Symbolic (Name name)) _ _ _ _) =
let
prefix0 =
"Var " ++ show name ++ " = "
prefix =
replicate (length prefix0) ' '
unfound = \case
EnvironmentValueNotFound _
-> "<<not found in environment>>"
EnvironmentTypeError _ _
-> "<<type representation in environment unexpected>>"
actual =
either unfound showPretty
$ reifyEnvironment env output
in
case lines actual of
[] ->
[prefix0 ++ "?"]
x : xs ->
(prefix0 ++ x) :
fmap (prefix ++) xs
instance Show (Sequential m state) where
show (Sequential xs) =
unlines $ concatMap renderAction xs
sequential ::
(MonadGen gen, MonadTest m)
=> Range Int
-> (forall v. state v)
-> [Command gen m state]
-> gen (Sequential m state)
sequential range initial commands =
fmap (Sequential . fst) $
genActions range commands (mkContext initial)
data Parallel m state =
Parallel {
parallelPrefix :: [Action m state]
, parallelBranch1 :: [Action m state]
, parallelBranch2 :: [Action m state]
}
instance Show (Parallel m state) where
show =
renderParallel renderAction
renderParallel :: (Action m state -> [String]) -> Parallel m state -> String
renderParallel render (Parallel pre xs ys) =
unlines $ concat [
["━━━ Prefix ━━━"]
, concatMap render pre
, ["", "━━━ Branch 1 ━━━"]
, concatMap render xs
, ["", "━━━ Branch 2 ━━━"]
, concatMap render ys
]
parallel ::
(MonadGen gen, MonadTest m)
=> Range Int
-> Range Int
-> (forall v. state v)
-> [Command gen m state]
-> gen (Parallel m state)
parallel prefixN parallelN initial commands = do
(prefix, ctx0) <- genActions prefixN commands (mkContext initial)
(branch1, ctx1) <- genActions parallelN commands ctx0
(branch2, _ctx2) <- genActions parallelN commands ctx1 { contextState = contextState ctx0 }
pure $ Parallel prefix branch1 branch2
data ActionCheck state =
ActionCheck {
checkUpdate :: state Concrete -> state Concrete
, checkEnsure :: state Concrete -> state Concrete -> Test ()
}
execute :: (MonadTest m, HasCallStack) => Action m state -> StateT Environment m (ActionCheck state)
execute (Action sinput soutput exec _require update ensure) =
withFrozenCallStack $ do
env0 <- get
input <- evalEither $ reify env0 sinput
output <- lift $ exec input
let
coutput =
Concrete output
env =
insertConcrete soutput coutput env0
put env
pure $
ActionCheck
(\s0 -> update s0 input (Var coutput))
(\s0 s -> ensure s0 s input output)
executeUpdateEnsure ::
(MonadTest m, HasCallStack)
=> (state Concrete, Environment)
-> Action m state
-> m (state Concrete, Environment)
executeUpdateEnsure (state0, env0) (Action sinput soutput exec _require update ensure) =
withFrozenCallStack $ do
input <- evalEither $ reify env0 sinput
output <- exec input
let
coutput =
Concrete output
state =
update state0 input (Var coutput)
env =
insertConcrete soutput coutput env0
liftTest $ ensure state0 state input output
pure (state, env)
executeSequential ::
(MonadTest m, MonadCatch m, HasCallStack)
=> (forall v. state v)
-> Sequential m state
-> m ()
executeSequential initial (Sequential xs) =
withFrozenCallStack $ evalM $
foldM_ executeUpdateEnsure (initial, emptyEnvironment) xs
successful :: Test () -> Bool
successful x =
case runTest x of
(Left _, _) ->
False
(Right _, _) ->
True
interleave :: [a] -> [a] -> [[a]]
interleave xs00 ys00 =
case (xs00, ys00) of
([], []) ->
[]
(xs, []) ->
[xs]
([], ys) ->
[ys]
(xs0@(x:xs), ys0@(y:ys)) ->
[ x : zs | zs <- interleave xs ys0 ] ++
[ y : zs | zs <- interleave xs0 ys ]
checkActions :: state Concrete -> [ActionCheck state] -> Test ()
checkActions s0 = \case
[] ->
pure ()
x : xs -> do
let
s =
checkUpdate x s0
checkEnsure x s0 s
checkActions s xs
linearize :: MonadTest m => state Concrete -> [ActionCheck state] -> [ActionCheck state] -> m ()
linearize initial branch1 branch2 =
withFrozenCallStack $
let
ok =
any successful .
fmap (checkActions initial) $
interleave branch1 branch2
in
if ok then
pure ()
else
failWith Nothing "no valid interleaving"
executeParallel ::
(MonadTest m, MonadCatch m, MonadBaseControl IO m, HasCallStack)
=> (forall v. state v)
-> Parallel m state
-> m ()
executeParallel initial p@(Parallel prefix branch1 branch2) =
withFrozenCallStack $ evalM $ do
(s0, env0) <- foldM executeUpdateEnsure (initial, emptyEnvironment) prefix
((xs, env1), (ys, env2)) <-
Async.concurrently
(runStateT (traverse execute branch1) env0)
(runStateT (traverse execute branch2) env0)
let
env = unionsEnvironment [env0, env1, env2]
annotate $ renderParallel (renderActionResult env) p
linearize s0 xs ys