quickcheck-lockstep-0.1.0: src/Test/QuickCheck/StateModel/Lockstep/API.hs
-- | Public API
--
-- This is re-exported through "Test.QuickCheck.StateModel.Lockstep".
module Test.QuickCheck.StateModel.Lockstep.API (
-- * State
Lockstep(..)
-- * Main abstraction
, InLockstep(..)
, RunLockstep(..)
-- * Convenience aliases
, LockstepAction
, ModelFindVariables
, ModelLookUp
, ModelVar
) where
import Data.Kind
import Data.Typeable
import Test.QuickCheck (Gen)
import Test.QuickCheck.StateModel (StateModel, Any, RunModel, Realized, Action)
import Test.QuickCheck.StateModel.Lockstep.EnvF (EnvF)
import Test.QuickCheck.StateModel.Lockstep.GVar (GVar, AnyGVar(..))
import Test.QuickCheck.StateModel.Lockstep.Op
import Test.QuickCheck.StateModel.Lockstep.Op.Identity qualified as Identity
{-------------------------------------------------------------------------------
Lockstep state
@quickcheck-dynamic@ takes care of keeping track of the responses of the
system under test, but not the model. We do that here.
Implementation note: the 'RunModel' class in @quickcheck-dynamic@ uses a type
family 'Realized': for an @Action state a@, the response from the real system
is expected to be of type @Realized m a@. This allows the same tests to be run
against different "test execution backends"; for example, we could run the
tests in the real IO monad, or using an IO monad simulator.
This is an orthogonal generalization to what Lockstep provides: no matter the
test execution backend, the /model/ will always be the same. We could perhaps
piggy-back on the same abstraction if we introduced a separate monad parameter
@n@ for the model, and then use @Realized n a@ instead of @ModelValue a@. This
might work, but it's less clear how to then also that for 'Observable'.
Overall, it seems cleaner to reserve 'Realized' exclusively for the
parameterization over test execution backends.
-------------------------------------------------------------------------------}
data Lockstep state = Lockstep {
lockstepModel :: state
, lockstepEnv :: EnvF (ModelValue state)
}
-- | The 'Show' instance does not show the internal environment
instance Show state => Show (Lockstep state) where
show = show . lockstepModel
{-------------------------------------------------------------------------------
Main lockstep abstraction
-------------------------------------------------------------------------------}
class
( StateModel (Lockstep state)
, Typeable state
, InterpretOp (ModelOp state) (ModelValue state)
, forall a. Show (ModelValue state a)
, forall a. Eq (Observable state a)
, forall a. Show (Observable state a)
)
=> InLockstep state where
-- | Values in the mock environment
--
-- 'ModelValue' witnesses the relation between values returned by the real
-- system and values returned by the model.
--
-- In most cases, we expect the real system and the model to return the
-- /same/ value. However, for some things we must allow them to diverge:
-- consider file handles for example.
data ModelValue state a
-- | Observable responses
--
-- The real system returns values of type @a@, and the model returns values
-- of type @MockValue a@. @Observable a@ defines the parts of those results
-- that expect to be the /same/ for both.
data Observable state a
-- | Type of operations required on the results of actions
--
-- Whenever an action has a result of type @a@, but we later need a variable
-- of type @b@, we need a constructor
--
-- > GetB :: ModelOp state a b
--
-- in the 'ModelOp' type. For many tests, the standard 'Op' type will
-- suffice, but not always.
type ModelOp state :: Type -> Type -> Type
type ModelOp state = Identity.Op
-- | Extract the observable part of a response from the model
observeModel :: ModelValue state a-> Observable state a
-- | All variables required by a command
usedVars :: LockstepAction state a -> [AnyGVar (ModelOp state)]
-- | Step the model
--
-- The order of the arguments mimicks 'perform'.
modelNextState ::
LockstepAction state a
-> ModelLookUp state
-> state
-> (ModelValue state a, state)
-- | Generate an arbitrary action, given a way to find variables
arbitraryWithVars ::
ModelFindVariables state
-> state
-> Gen (Any (LockstepAction state))
-- | Shrink an action, given a way to find variables
--
-- This is optional; without an implementation of 'shrinkWithVars', lists of
-- actions will still be pruned, but /individual/ actions will not be shrunk.
shrinkWithVars ::
ModelFindVariables state
-> state
-> LockstepAction state a
-> [Any (LockstepAction state)]
shrinkWithVars _ _ _ = []
-- | Tag actions
--
-- Tagging is optional, but can help understand your test input data as
-- well as your shrinker (see 'tagActions').
tagStep ::
(state, state)
-> LockstepAction state a
-> ModelValue state a
-> [String]
tagStep _ _ _ = []
class ( InLockstep state
, RunModel (Lockstep state) m
) => RunLockstep state m where
-- See also 'Observable'
observeReal ::
Proxy m
-> LockstepAction state a -> Realized m a -> Observable state a
{-------------------------------------------------------------------------------
Convenience aliases
-------------------------------------------------------------------------------}
-- | An action in the lock-step model
type LockstepAction state = Action (Lockstep state)
-- | Look up a variable for model execution
--
-- The type of the variable is the type in the /real/ system.
type ModelLookUp state = forall a. ModelVar state a -> ModelValue state a
-- | Find variables of the appropriate type
--
-- The type you pass must be the result type of (previously executed) actions.
-- If you want to change the type of the variable, see
-- 'StateModel.Lockstep.GVar.map'.
type ModelFindVariables state = forall a.
Typeable a
=> Proxy a -> [GVar (ModelOp state) a]
-- | Variables with a "functor-esque" instance
type ModelVar state = GVar (ModelOp state)