Skip to content

Commit de24951

Browse files
Testing-interface: Generalize base monad
1 parent 17defed commit de24951

File tree

1 file changed

+68
-52
lines changed

1 file changed

+68
-52
lines changed

src/testing-interface/lib/Convex/TestingInterface.hs

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module Convex.TestingInterface (
6060
) where
6161

6262
import Control.Monad (foldM, forM, unless, when)
63-
import Control.Monad.IO.Class (liftIO)
63+
import Control.Monad.IO.Class (MonadIO, liftIO)
6464
import Test.HUnit (Assertion)
6565
import Test.QuickCheck (Arbitrary (..), Gen, Property, counterexample, discard, elements, frequency, oneof, property)
6666
import Test.QuickCheck.Monadic (monadicIO, monitor, run)
@@ -118,74 +118,84 @@ correctly.
118118
Minimal complete definition: 'Action', 'initialState', 'arbitraryAction', 'nextState', 'perform'
119119
-}
120120
class (Show state, Eq state) => TestingInterface state where
121-
-- | Actions that can be performed on the contract.
122-
-- This is typically a data type with one constructor per contract operation.
121+
{- | Actions that can be performed on the contract.
122+
This is typically a data type with one constructor per contract operation.
123+
-}
123124
data Action state
124125

125126
-- | The initial state of the model, before any actions are performed.
126127
initialState :: state
127128

128-
-- | Generate a random action given the current state.
129-
-- The generated action should be appropriate for the current state.
129+
{- | Generate a random action given the current state.
130+
The generated action should be appropriate for the current state.
131+
-}
130132
arbitraryAction :: state -> Gen (Action state)
131133

132-
-- | Precondition that must hold before an action can be executed.
133-
-- Return 'False' to indicate that an action is not valid in the current state.
134-
-- Default: all actions are always valid.
134+
{- | Precondition that must hold before an action can be executed.
135+
Return 'False' to indicate that an action is not valid in the current state.
136+
Default: all actions are always valid.
137+
-}
135138
precondition :: state -> Action state -> Bool
136139
precondition _ _ = True
137140

138-
-- | Update the model state after an action is performed.
139-
-- This should reflect the expected effect of the action on the contract state.
141+
{- | Update the model state after an action is performed.
142+
This should reflect the expected effect of the action on the contract state.
143+
-}
140144
nextState :: state -> Action state -> state
141145

142-
-- | Perform the action on the real blockchain (mockchain).
143-
-- This should execute the actual transaction(s) that implement the action.
144-
-- The current model state is provided to allow access to tracked blockchain state.
146+
{- | Perform the action on the real blockchain (mockchain).
147+
This should execute the actual transaction(s) that implement the action.
148+
The current model state is provided to allow access to tracked blockchain state.
149+
-}
145150
perform :: state -> Action state -> TestingMonadT IO ()
146151

147-
-- | Validate that the blockchain state matches the model state.
148-
-- Default: no validation (always succeeds).
152+
{- | Validate that the blockchain state matches the model state.
153+
Default: no validation (always succeeds).
154+
-}
149155
validate :: state -> TestingMonadT IO Bool
150156
validate _ = pure True
151157

152-
-- | Called after each action to check custom properties.
153-
-- Default: no additional checks.
158+
{- | Called after each action to check custom properties.
159+
Default: no additional checks.
160+
-}
154161
monitoring :: state -> Action state -> Property -> Property
155162
monitoring _ _ = id
156163

157-
-- | Threat models to run against the last transaction.
158-
-- Each threat model will be evaluated against the final transaction
159-
-- with the UTxO state captured before that transaction executed.
160-
-- Default: no threat models.
164+
{- | Threat models to run against the last transaction.
165+
Each threat model will be evaluated against the final transaction
166+
with the UTxO state captured before that transaction executed.
167+
Default: no threat models.
168+
-}
161169
threatModels :: [ThreatModel ()]
162170
threatModels = []
163171

164-
-- | Threat models that are expected to find vulnerabilities.
165-
-- These are run like 'threatModels' but with inverted pass/fail semantics:
166-
--
167-
-- * OK when a vulnerability IS detected
168-
-- * FAIL when a vulnerability is NOT detected
169-
--
170-
-- Output is quiet — no verbose transaction dumps.
171-
-- Default: empty, backward compatible.
172+
{- | Threat models that are expected to find vulnerabilities.
173+
These are run like 'threatModels' but with inverted pass/fail semantics:
174+
175+
* OK when a vulnerability IS detected
176+
* FAIL when a vulnerability is NOT detected
177+
178+
Output is quiet — no verbose transaction dumps.
179+
Default: empty, backward compatible.
180+
-}
172181
expectedVulnerabilities :: [ThreatModel ()]
173182
expectedVulnerabilities = []
174183

175-
-- | Whether to discard (skip) test cases where the invalid action fails due to
176-
-- a user-level error (e.g., off-chain balancing failure) rather than an
177-
-- on-chain validator rejection during negative testing.
178-
--
179-
-- When 'True', negative tests that throw user exceptions are discarded
180-
-- (via QuickCheck's 'discard'), so only on-chain rejections count as
181-
-- successful negative tests.
182-
--
183-
-- When 'False' (the default), user exceptions also cause the test case
184-
-- to be discarded — meaning both off-chain and on-chain failures are
185-
-- treated the same way.
186-
--
187-
-- Override this in your 'TestingInterface' instance if you need finer
188-
-- control over which failure modes are accepted in negative testing.
184+
{- | Whether to discard (skip) test cases where the invalid action fails due to
185+
a user-level error (e.g., off-chain balancing failure) rather than an
186+
on-chain validator rejection during negative testing.
187+
188+
When 'True', negative tests that throw user exceptions are discarded
189+
(via QuickCheck's 'discard'), so only on-chain rejections count as
190+
successful negative tests.
191+
192+
When 'False' (the default), user exceptions also cause the test case
193+
to be discarded — meaning both off-chain and on-chain failures are
194+
treated the same way.
195+
196+
Override this in your 'TestingInterface' instance if you need finer
197+
control over which failure modes are accepted in negative testing.
198+
-}
189199
discarNegativeTestForUserExceptions :: Bool
190200
discarNegativeTestForUserExceptions = False
191201

@@ -204,11 +214,14 @@ newtype TestingMonadT m a = TestingMonad
204214
, C.MonadError (BalanceTxError C.ConwayEra)
205215
, C.MonadIO
206216
, MonadLog
207-
, MonadFail
208217
, MonadBlockchain C.ConwayEra
209218
, MonadMockchain C.ConwayEra
210219
)
211220

221+
-- Let the TestingMonad fail in IO
222+
instance (MonadIO m) => MonadFail (TestingMonadT m) where
223+
fail s = liftIO $ fail s
224+
212225
-- | Opaque wrapper for model state
213226
newtype ModelState state = ModelState {unModelState :: state}
214227
deriving (Eq, Show)
@@ -277,8 +290,9 @@ data RunOptions = RunOptions
277290
-- ^ Maximum number of actions to generate
278291
, mcOptions :: Options C.ConwayEra
279292
, disableNegativeTesting :: Maybe String
280-
-- ^ If @Just reason@, negative tests are skipped (shown as IGNORED) with the given reason.
281-
-- If @Nothing@, negative tests run normally. Default: @Nothing@.
293+
{- ^ If @Just reason@, negative tests are skipped (shown as IGNORED) with the given reason.
294+
If @Nothing@, negative tests run normally. Default: @Nothing@.
295+
-}
282296
}
283297

284298
defaultRunOptions :: RunOptions
@@ -619,11 +633,11 @@ expectedVulnTestCase getTmResultsRef idx tm =
619633

620634
-- | Execute a single action and update the model state
621635
runAction
622-
:: (TestingInterface state, Show (Action state))
636+
:: (TestingInterface state, Show (Action state), MonadIO m)
623637
=> RunOptions
624638
-> state
625639
-> Action state
626-
-> TestingMonadT IO state
640+
-> TestingMonadT m state
627641
runAction opts modelState action = do
628642
when (verbose opts) $
629643
liftIO $
@@ -654,11 +668,13 @@ Use with 'withCoverage' to set up coverage tracking for your test suite.
654668
-}
655669
data CoverageConfig = CoverageConfig
656670
{ coverageIndices :: [CoverageIndex]
657-
-- ^ Coverage indices from compiled scripts (obtained via @'PlutusTx.Code.getCovIdx'@).
658-
-- Multiple indices are combined with @'<>'@.
671+
{- ^ Coverage indices from compiled scripts (obtained via @'PlutusTx.Code.getCovIdx'@).
672+
Multiple indices are combined with @'<>'@.
673+
-}
659674
, coverageReport :: CoverageReport -> IO ()
660-
-- ^ Action to perform with the final coverage report.
661-
-- Use 'printCoverageReport', 'writeCoverageReport', or 'silentCoverageReport'.
675+
{- ^ Action to perform with the final coverage report.
676+
Use 'printCoverageReport', 'writeCoverageReport', or 'silentCoverageReport'.
677+
-}
662678
}
663679

664680
-- | Print a coverage report to stdout using prettyprinter.

0 commit comments

Comments
 (0)