@@ -31,10 +31,6 @@ module Convex.TestingInterface (
3131 defaultOptions ,
3232 modifyTransactionLimits ,
3333
34- -- * Actions
35- Actions (Actions ),
36- InvalidActions (.. ),
37-
3834 -- * Coverage helpers
3935 withCoverage ,
4036 CoverageConfig (.. ),
@@ -63,7 +59,7 @@ import Control.Monad (foldM, forM, unless, when)
6359import Control.Monad.IO.Class (MonadIO , liftIO )
6460import Test.HUnit (Assertion )
6561import Test.QuickCheck (Arbitrary (.. ), Gen , Property , counterexample , discard , elements , frequency , oneof , property )
66- import Test.QuickCheck.Monadic (monadicIO , monitor , run )
62+ import Test.QuickCheck.Monadic (PropertyM , monadicIO , monitor , pick , run )
6763import Test.Tasty (DependencyType (.. ), TestTree , sequentialTestGroup , testGroup , withResource )
6864import Test.Tasty.ExpectedFailure (ignoreTestBecause )
6965import Test.Tasty.HUnit (assertFailure , testCaseSteps )
@@ -76,7 +72,7 @@ import Control.Lens ((&), (.~), (^.))
7672import Control.Monad.Trans (MonadTrans (.. ))
7773import Convex.Class (MonadBlockchain , MonadMockchain , coverageData , getTxs )
7874import Convex.CoinSelection (BalanceTxError , coverageFromBalanceTxError )
79- import Convex.MockChain (MockChainState (.. ), MockchainT , initialStateFor , runMockchain0IOWith , runMockchainIO )
75+ import Convex.MockChain (MockChainState (.. ), MockchainT , initialStateFor , runMockchainIO , runMockchainT )
8076import Convex.MockChain.Defaults qualified as Defaults
8177import Convex.MonadLog (MonadLog )
8278import Convex.NodeParams (NodeParams (.. ))
@@ -116,7 +112,7 @@ The type parameter @state@ represents the model's view of the world. It should
116112track all relevant information needed to validate that the contract is behaving
117113correctly.
118114
119- Minimal complete definition: 'Action', 'initialState ', 'arbitraryAction', 'nextState', 'perform'
115+ Minimal complete definition: 'Action', 'initialize ', 'arbitraryAction', 'nextState', 'perform'
120116-}
121117class (Show state , Eq state ) => TestingInterface state where
122118 {- | Actions that can be performed on the contract.
@@ -125,7 +121,7 @@ class (Show state, Eq state) => TestingInterface state where
125121 data Action state
126122
127123 -- | The initial state of the model, before any actions are performed.
128- initialState :: state
124+ initialize :: ( MonadIO m ) => TestingMonadT m state
129125
130126 {- | Generate a random action given the current state.
131127 The generated action should be appropriate for the current state.
@@ -205,8 +201,8 @@ class (Show state, Eq state) => TestingInterface state where
205201Leaving handling of balancing errors to the testing interface is important because
206202the errors can contain data for code coverage.
207203-}
208- newtype TestingMonadT m a = TestingMonad
209- { runTestingMonadT :: ExceptT (BalanceTxError C. ConwayEra ) (MockchainT C. ConwayEra m ) a
204+ newtype TestingMonadT m a = TestingMonadT
205+ { unTestingMonadT :: ExceptT (BalanceTxError C. ConwayEra ) (MockchainT C. ConwayEra m ) a
210206 }
211207 deriving newtype
212208 ( Functor
@@ -219,12 +215,19 @@ newtype TestingMonadT m a = TestingMonad
219215 , MonadMockchain C. ConwayEra
220216 )
221217
218+ runTestingMonadT
219+ :: NodeParams C. ConwayEra
220+ -> TestingMonadT m a
221+ -> m (Either (BalanceTxError C. ConwayEra ) a , MockChainState C. ConwayEra )
222+ runTestingMonadT params (TestingMonadT action) =
223+ runMockchainT (runExceptT action) params (initialStateFor params Wallet. initialUTxOs)
224+
222225-- Let the TestingMonad fail in IO
223226instance (MonadIO m ) => MonadFail (TestingMonadT m ) where
224227 fail s = liftIO $ fail s
225228
226229instance MonadTrans TestingMonadT where
227- lift = TestingMonad . lift . lift
230+ lift = TestingMonadT . lift . lift
228231
229232-- | Opaque wrapper for model state
230233newtype ModelState state = ModelState { unModelState :: state }
@@ -235,36 +238,6 @@ Key is the threat model name, value is the list of outcomes (one per iteration).
235238-}
236239type ThreatModelResults = Map. Map String [ThreatModelOutcome ]
237240
238- -- | A sequence of actions to perform
239- newtype Actions state = Actions_ [Action state ]
240-
241- newtype InvalidActions state = InvalidActions (Actions state , Action state )
242-
243- instance (TestingInterface state , Show (Action state )) => Show (InvalidActions state ) where
244- show (InvalidActions (Actions prefix, bad)) =
245- " InvalidActions " ++ show prefix ++ " then " ++ show bad
246-
247- pattern Actions :: [Action state ] -> Actions state
248- pattern Actions as = Actions_ as
249- {-# COMPLETE Actions #-}
250-
251- instance (TestingInterface state , Show (Action state )) => Show (Actions state ) where
252- show (Actions acts) = " Actions " ++ show acts
253-
254- instance (TestingInterface state ) => Arbitrary (Actions state ) where
255- arbitrary = Actions <$> genActions initialState 10
256-
257- instance (TestingInterface state ) => Arbitrary (InvalidActions state ) where
258- arbitrary = do
259- -- Generate a valid prefix (builds up state)
260- prefix <- genActions initialState 10
261- let finalState = foldl nextState initialState prefix
262- -- Generate an action that VIOLATES the precondition in that state
263- maybeInvalid <- arbitraryAction finalState `suchThatMaybe` (not . precondition finalState)
264- case maybeInvalid of
265- Nothing -> discard -- tell QuickCheck to skip this case
266- Just bad -> pure $ InvalidActions (Actions_ prefix, bad)
267-
268241-- | Try up to 100 times to generate a value satisfying a predicate
269242suchThatMaybe :: Gen a -> (a -> Bool ) -> Gen (Maybe a )
270243suchThatMaybe gen p = go (100 :: Int )
@@ -359,26 +332,32 @@ negativeTest
359332 :: forall state
360333 . (TestingInterface state , Show (Action state ))
361334 => RunOptions
362- -> InvalidActions state
363335 -> Property
364- negativeTest opts ( InvalidActions ( Actions actions, badAction)) = monadicIO $ do
336+ negativeTest opts = monadicIO $ do
365337 let RunOptions {mcOptions = Options {coverageRef, params}} = opts
366- initialSt = initialState @ state
338+ -- Phase 1: Run the valid prefix, capturing the final mockchain state
339+ (prefixResult, prefixState) <- runTestingMonadT params $ do
340+ initialState <- runInitialization @ state opts
367341
368- when (verbose opts) $
369- monitor (counterexample $ " Initial state: " ++ show initialSt)
342+ (actions, badAction) <- lift $ pick $ do
343+ -- Generate a valid prefix (builds up state)
344+ prefix <- genActions initialState 10
345+ let finalState = foldl nextState initialState prefix
346+ -- Generate an action that VIOLATES the precondition in that state
347+ maybeInvalid <- arbitraryAction finalState `suchThatMaybe` (not . precondition finalState)
348+ case maybeInvalid of
349+ Nothing -> discard -- tell QuickCheck to skip this case
350+ Just bad -> pure (prefix, bad)
370351
371- -- Phase 1: Run the valid prefix, capturing the final mockchain state
372- (prefixResult, prefixState) <- run $ runMockchain0IOWith Wallet. initialUTxOs params $ runExceptT $ runTestingMonadT $ do
373- foldM (runAction opts) initialSt actions
352+ ((,) badAction) <$> foldM (runAction opts) initialState actions
374353
375354 -- Phase 2: Run the bad action starting from the state left by the valid prefix
376355 case prefixResult of
377356 Left err -> do
378357 monitor (counterexample $ " Valid prefix failed: " ++ show err)
379358 pure (property False )
380- Right finalState -> do
381- let monadAction = runExceptT $ runTestingMonadT $ perform finalState badAction
359+ Right (badAction, finalState) -> do
360+ let monadAction = runExceptT $ unTestingMonadT $ perform finalState badAction
382361 result' <- run $ try @ SomeException $ runMockchainIO monadAction params prefixState
383362 -- We distinguish between validation errors and user errors:
384363 -- if the action failed at the off-chain level (e.g. balancing), we discard the test,
@@ -413,17 +392,14 @@ positiveTest
413392 -- ^ Threat models (early-stop on TMFailed)
414393 -> [ThreatModel () ]
415394 -- ^ Expected vulnerabilities (never early-stop)
416- -> Actions state
417395 -> Property
418- positiveTest opts mGetTmResultsRef tms evs ( Actions actions) = monadicIO $ do
396+ positiveTest opts mGetTmResultsRef tms evs = monadicIO $ do
419397 let RunOptions {mcOptions = Options {coverageRef, params}} = opts
420- initialSt = initialState @ state
421-
422- when (verbose opts) $
423- monitor (counterexample $ " Initial state: " ++ show initialSt)
398+ result <- runTestingMonadT params $ do
399+ initialState <- runInitialization @ state opts
400+ actions <- lift $ pick $ genActions initialState 10
424401
425- result <- run $ runMockchain0IOWith Wallet. initialUTxOs params $ runExceptT $ runTestingMonadT $ do
426- finalState <- foldM (runAction opts) initialSt actions
402+ finalState <- foldM (runAction opts) initialState actions
427403
428404 -- Run threat models in isolation
429405 -- Note: runThreatModelCheck handles rebalancing failures internally (returns TMSkipped)
@@ -652,6 +628,25 @@ runAction opts modelState action = do
652628
653629 pure modelState'
654630
631+ -- | Initialize the blockchain and validate the model state
632+ runInitialization
633+ :: forall state m
634+ . (TestingInterface state , MonadIO m )
635+ => RunOptions
636+ -> TestingMonadT (PropertyM m ) state
637+ runInitialization opts = do
638+ initialState <- initialize @ state
639+
640+ when (verbose opts) $
641+ lift $
642+ monitor (counterexample $ " Initial state: " ++ show initialState)
643+
644+ valid <- validate initialState
645+ unless valid $
646+ fail " Blockchain state does not match model state after initialization"
647+
648+ pure initialState
649+
655650{- | Configuration for coverage collection and reporting.
656651
657652Use with 'withCoverage' to set up coverage tracking for your test suite.
@@ -870,7 +865,7 @@ modifyTransactionLimits opts@Options{params = Defaults.pParams -> pp} newVal =
870865-- | Run the 'TestingMonadT' action with the given options and fail if there is an error
871866mockchainSucceedsWithOptions :: Options C. ConwayEra -> TestingMonadT IO a -> Assertion
872867mockchainSucceedsWithOptions Options {params, coverageRef} action =
873- runMockchain0IOWith Wallet. initialUTxOs params (runExceptT (runTestingMonadT action))
868+ runTestingMonadT params action
874869 >>= \ (res, st) -> do
875870 let covData = st ^. coverageData
876871 for_ coverageRef $ \ ref -> modifyIORef ref (<> covData)
@@ -885,7 +880,7 @@ mockchainSucceedsWithOptions Options{params, coverageRef} action =
885880-}
886881mockchainFailsWithOptions :: Options C. ConwayEra -> TestingMonadT IO a -> (BalanceTxError C. ConwayEra -> Assertion ) -> Assertion
887882mockchainFailsWithOptions Options {params, coverageRef} action handleError =
888- runMockchain0IOWith Wallet. initialUTxOs params (runExceptT (runTestingMonadT action))
883+ runTestingMonadT params action
889884 >>= \ (res, st) -> do
890885 let covData = st ^. coverageData
891886 for_ coverageRef $ \ ref -> modifyIORef ref (<> covData)
0 commit comments