Safe Haskell | None |
---|---|
Language | Haskell2010 |
Synopsis
- type DL state = DL (ModelState state)
- class ActionLike state a | a -> state where
- observe :: ContractModel state => String -> ((forall t. HasSymbolicRep t => Symbolic t -> t) -> ChainState -> Bool) -> DL state ()
- waitUntilDL :: forall state. ContractModel state => SlotNo -> DL state ()
- anyAction :: DL state ()
- anyActions :: Int -> DL state ()
- anyActions_ :: DL state ()
- stopping :: DL state ()
- weight :: Double -> DL state ()
- getSize :: DL state Int
- monitor :: (Property -> Property) -> DL state ()
- assertModel :: String -> (ModelState state -> Bool) -> DL state ()
- forAllDL :: (ContractModel state, Testable p) => DL state () -> (Actions state -> p) -> Property
- forAllUniqueDL :: forall state p. (ContractModel state, Testable p) => Annotated (ModelState state) -> DL state () -> (Actions state -> p) -> Property
Documentation
Test scenarios are described in the DL
monad (based on dynamic logic) which lets you freely mix
random sequences of actions (anyAction
, anyActions_
, anyActions
) with specific
actions (action
). It also supports checking properties of the model state (assert
,
assertModel
), and random generation (forAllQ
).
For instance, a unit test for a simple auction contract might look something like this:
unitTest ::DL
AuctionState () unitTest = doaction
$ Bid w1 100action
$ Bid w2 150action
$ Wait endSlotaction
$ Collect
and could easily be extended with some randomly generated values
unitTest ::DL
AuctionState () unitTest = do bid <-forAllQ
$chooseQ
(1, 100)action
$ Bid w1 bidaction
$ Bid w2 (bid + 50)action
$ Wait endSlotaction
$ Collect
More interesting scenarios can be constructed by mixing random and fixed sequences. The following checks that you can always finish an auction after which point there are no funds locked by the contract:
finishAuction ::DL
AuctionState () finishAuction = doanyActions_
action
$ Wait endSlotaction
$ CollectassertModel
"Funds are locked!" (isZero
.lockedValue
)
DL
scenarios are turned into QuickCheck properties using forAllDL
.
In addition to failing the check that the emulator run matches the model, there are a few other ways that test scenarios can fail:
- an explicit
action
does not satisfy itsprecondition
- a failed
assert
orassertModel
, or a monadfail
- an
empty
set ofAlternative
s - the scenario fails to terminate (see
stopping
)
All of these occur at test case generation time, and thus do not directly say anything about the
contract implementation. However, together with the check that the model agrees with the emulator
they indirectly imply properties of the implementation. An advantage of this is that DL
test
scenarios can be checked without running the contract through the emulator, which is much much
faster. For instance,
prop_FinishModel = forAllDL
finishAuction $ const True
would check that the model does not think there will be any locked funds after the auction is finished. Once this property passes, one can run the slower property that also checks that the emulator agrees.
type DL state = DL (ModelState state) Source #
The monad for writing test scenarios. It supports non-deterministic choice through
Alternative
, failure with MonadFail
, and access to the model state
through GetModelState
. It is lazy, so scenarios can be potentially infinite, although the
probability of termination needs to be high enough that concrete test cases are always finite.
See stopping
for more information on termination.
class ActionLike state a | a -> state where Source #
action :: a -> DL state () Source #
Generate a specific action. Fails if the action's precondition
is not satisfied.
Instances
ContractModel state => ActionLike state (Action state) Source # | |
(ContractModel state, Typeable a) => ActionLike state (Action (ModelState state) a) Source # | |
Defined in Test.QuickCheck.ContractModel.DL action :: Action (ModelState state) a -> DL state () Source # |
observe :: ContractModel state => String -> ((forall t. HasSymbolicRep t => Symbolic t -> t) -> ChainState -> Bool) -> DL state () Source #
waitUntilDL :: forall state. ContractModel state => SlotNo -> DL state () Source #
anyAction :: DL state () Source #
Generate a random action using arbitraryAction
. The generated action is guaranteed to satisfy
its precondition
. Fails with Stuck
if no action satisfying the precondition can be found
after 100 attempts.
anyActions :: Int -> DL state () Source #
Generate a sequence of random actions using arbitraryAction
. All actions satisfy their
precondition
s. The argument is the expected number of actions in the sequence chosen from a
geometric distribution, unless in the stopping
stage, in which case as few actions as
possible are generated.
anyActions_ :: DL state () Source #
Generate a sequence of random actions using arbitraryAction
. All actions satisfy their
precondition
s. Actions may be generated until the stopping
stage is reached; the expected length is size/2.
stopping :: DL state () Source #
Test case generation from DL
scenarios have a target length of the action sequence to be
generated that is based on the QuickCheck size parameter (see sized
). However, given that
scenarios can contain explicit action
s it might not be possible to stop the scenario once the
target length has been reached.
Instead, once the target number of actions have been reached, generation goes into the
stopping phase. In this phase branches starting with stopping
are preferred, if possible.
Conversely, before the stopping phase, branches starting with stopping
are avoided unless there are no other possible choices.
For example, here is the definition of anyActions
:
anyActions
n =stopping
<|>
pure ()<|>
(weight
(fromIntegral n) >>anyAction
>>anyActions
n)
The effect of this definition is that the second or third branch will be taken until the desired number
of actions have been generated, at which point the stopping
branch will be taken and
generation stops (or continues with whatever comes after the anyActions
call).
Now, it might not be possible, or too hard, to find a way to terminate a scenario. For instance, this scenario has no finite test cases:
looping = anyAction
>> looping
To prevent test case generation from looping, if a scenario has not terminated after generating
2 * n + 20
actions, where n
is when the stopping phase kicks in, generation fails with a
Looping
error.
weight :: Double -> DL state () Source #
By default, Alternative
choice (<|>
) picks among
the next actions with equal probability. So, for instance, this code chooses between the actions
a
, b
and c
, with a probability 1/3
of choosing each:
unbiasedChoice a b c =action
a<|>
action
b<|>
action
c
To change this you can use weight
, which multiplies the
relative probability of picking a branch by the given number.
For instance, the following scenario picks the action a
with probability 2/3
and the action
b
with probability 1/3
:
biasedChoice a b =weight
2 (action
a)<|>
weight
(action
b)
Calls to weight
need to appear at the top-level after a choice, preceding any actions
(action
/anyAction
) or random generation (forAllQ
), or they will have no effect.
getSize :: DL state Int Source #
Sometimes test case generation should depend on QuickCheck's size
parameter. This can be accessed using getSize
. For example, anyActions_
is defined by
anyActions_ = do n <- getSize
anyActions (n div
2 + 1)
so that we generate a random number of actions, but on average half the size (which is about the same as the average random positive integer, or length of a list).
monitor :: (Property -> Property) -> DL state () Source #
The monitor
function allows you to collect statistics of your testing using QuickCheck
functions like label
, collect
, classify
,
and tabulate
. See also the monitoring
method of ContractModel
which is
called for all actions in a test case (regardless of whether they are generated by an explicit
action
or an anyAction
).
assertModel :: String -> (ModelState state -> Bool) -> DL state () Source #
Fail unless the given predicate holds of the model state.
Equivalent to
assertModel msg p = do s <-getModelState
assert
msg (p s)
forAllDL :: (ContractModel state, Testable p) => DL state () -> (Actions state -> p) -> Property Source #
Turn a DL
scenario into a QuickCheck property. Generates a random Actions
matching the
scenario and feeds it to the given property. The property can be a full property running the
emulator and checking the results, defined using propRunActions_
, propRunActions
, or
propRunActionsWithOptions
. Assuming a model for an auction contract and DL
scenario that
checks that you can always complete the auction, you can write:
finishAuction ::DL
AuctionState () prop_Auction =propRunActions_
handles where handles = ... prop_Finish =forAllDL
finishAuction prop_Auction
However, there is also value in a property that does not run the emulator at all:
prop_FinishModel = forAllDL
finishAuction $ const True
This will check all the assertions and other failure conditions of the DL
scenario very
quickly. Once this property passes a large number of tests, you can run the full property
checking that the model agrees with reality.
forAllUniqueDL :: forall state p. (ContractModel state, Testable p) => Annotated (ModelState state) -> DL state () -> (Actions state -> p) -> Property Source #
Orphan instances
GetModelState (DL state) Source # | |
ContractModel s => DynLogicModel (ModelState s) Source # | |
restricted :: Action (ModelState s) a -> Bool |