-- SPDX-FileCopyrightText: 2020 Tocqueville Group -- -- SPDX-License-Identifier: LicenseRef-MIT-TQ -- | Utils for testing Indigo module Test.Util ( testIndigoContract , testIndigo , testIndigoDoc , zeroDivFail , notNewKeyFail , notNewKeyM , negativeResFail , negativeResM , validateContract , validateContractOps , validateContractSt , validateContractConst , validateStSuccess , validateStEither , validateStack2 , noOptimizationContract ) where import qualified Data.Text.IO.Utf8 as Utf8 (readFile) import Fmt (pretty) import Hedgehog (Gen, MonadTest, PropertyT, annotate, forAll, property, (===)) import Prelude import Test.HUnit ((@?=)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.Hedgehog (testProperty) import Test.Tasty.HUnit (testCase) import Indigo.Lorentz import Lorentz.Run (Contract(..)) import Lorentz.Test (ContractPropValidator, contractProp, dummyContractEnv, failedTest) import Michelson.Interpret (MichelsonFailed(..)) import Michelson.Typed.Haskell.Value (IsoValuesStack) type IndigoInstrValidator m pm st out = pm -> st -> Either MichelsonFailed (Rec Identity out) -> m () -------------------------------------------------------------------------------- -- Tests -------------------------------------------------------------------------------- -- | Takes a validating function, an Indigo-generated contract and a Lorentz -- contract and checks that the two have equivalent code (aka made of the same -- instructions) and that it passes validation for random param and storage. testIndigoContract :: forall pm st. ( Show pm, Show st , NiceParameterFull pm, NiceStorage st ) => String -> Gen pm -> Gen st -> (pm -> st -> ContractPropValidator (ToT st) (PropertyT IO ())) -> ContractCode pm st -> FilePath -> TestTree testIndigoContract name genPm genSt propValidator iContract michelsonFile = testGroup ("Indigo contract: " <> name) [ testCase "matches Michelson reference contract" $ do expectedContract <- Utf8.readFile michelsonFile printLorentzContract False iContractWithoutOptimization @?= fromStrict expectedContract , testProperty "has the correct resulting state and operations" $ property $ do pm <- forAll genPm st <- forAll genSt contProp pm st ] where iContractWithoutOptimization = noOptimizationContract iContract contProp :: pm -> st -> PropertyT IO () contProp param storage = withDict (niceParameterEvi @pm) $ contractProp (compileLorentzContract iContractWithoutOptimization) (propValidator param storage) dummyContractEnv param storage testIndigoDoc :: forall pm st. String -> ContractCode pm st -> ContractCode pm st -> TestTree testIndigoDoc name iContract lContract = testCase (name <> " matches Lorentz docs content") $ renderLorentzDoc iContract @?= renderLorentzDoc lContract -- | Takes a validating function and an Indigo-generated Lorentz `Instr` to check -- the resulting stack content. testIndigo :: (Show pm, Show st, IsoValue pm, IsoValue st, IsoValuesStack out) => String -> Gen pm -> Gen st -> IndigoInstrValidator (PropertyT IO) pm st out -> ('[pm, st] :-> out) -> TestTree testIndigo name genPm genSt validator iInstr = testProperty (name <> " Indigo Expr has correct resulting stack") $ property $ do pm <- forAll genPm st <- forAll genSt stackProp pm st where stackProp param storage = validator param storage . interpretLorentzInstr dummyContractEnv iInstr $ Identity param :& Identity storage :& RNil -------------------------------------------------------------------------------- -- Common failures -------------------------------------------------------------------------------- zeroDivFail :: MichelsonFailed zeroDivFail = errorToVal [mt|devision by zero|] MichelsonFailedWith notNewKeyFail :: MichelsonFailed notNewKeyFail = errorToVal notNewKeyM MichelsonFailedWith notNewKeyM :: MText notNewKeyM = [mt|not new key|] negativeResFail :: MichelsonFailed negativeResFail = errorToVal negativeResM MichelsonFailedWith negativeResM :: MText negativeResM = [mt|unacceptable negative result|] -------------------------------------------------------------------------------- -- Contract Validators -------------------------------------------------------------------------------- -- | Makes a validator for `testIndigoContract` that can expect a failure or a -- resulting [Operation] from the given function. Ignores new storage value. validateContract :: MonadTest m => IsoValue st => (pm -> st -> Either MichelsonFailed ([Operation], st)) -> pm -> st -> ContractPropValidator (ToT st) (m ()) validateContract fn param st (res, _) = assertMichelsonResult (fn param st) res $ \(ops, val) (opsRes, resVal) -> do annotate "matches resulting Storage and Operations" (ops, toVal val) === (opsRes, resVal) -- | Makes a validator for `testIndigoContract` that can expect a failure or a -- resulting [Operation] from the given function. Ignores new storage value. validateContractOps :: MonadTest m => (pm -> st -> Either MichelsonFailed [Operation]) -> pm -> st -> ContractPropValidator (ToT st) (m ()) validateContractOps fn param st (res, _) = assertMichelsonResult (fn param st) res $ \ops (opsRes, _) -> do annotate "matches resulting Operations" ops === opsRes -- | Makes a validator for `testIndigoContract` that can expect a failure or a -- new storage from the given function. Ignores resulting [Operation] validateContractSt :: MonadTest m => IsoValue st => (pm -> st -> Either MichelsonFailed st) -> pm -> st -> ContractPropValidator (ToT st) (m ()) validateContractSt fn param st (res, _) = assertMichelsonResult (fn param st) res $ \val (_, resVal) -> do annotate "matches resulting Storage" toVal val === resVal -- | Validator for `testIndigoContract` that expects the storage to remain -- the same and the resulting [Operation] to be empty. Ignores the parameter. validateContractConst :: MonadTest m => IsoValue st => pm -> st -> ContractPropValidator (ToT st) (m ()) validateContractConst = validateContract (\_param st -> Right ([], st)) -------------------------------------------------------------------------------- -- Instr Validators -------------------------------------------------------------------------------- -- | Makes a validator for `testIndigo` that expects the stack not to change type -- and the "storage" value to have changed as described by the given function. -- Resulting "param" is ignored. validateStSuccess :: (MonadTest m, Eq st, Show st) => (pm -> st -> st) -> IndigoInstrValidator m pm st '[pm, st] validateStSuccess fn = validateStEither (\p s -> Right $ fn p s) -- | Makes a validator for `testIndigo` that expects the stack not to change type -- and Either end with a failure or with a new stack. Resulting "param" is ignored. validateStEither :: forall m st pm. (MonadTest m, Eq st, Show st) => (pm -> st -> Either MichelsonFailed st) -> IndigoInstrValidator m pm st '[pm, st] validateStEither fn param st res = assertMichelsonResult (fn param st) res checkSt where checkSt :: st -> (Rec Identity '[pm, st]) -> m () checkSt val resStack = do let Identity _ :& Identity newState :& RNil = resStack annotate "matches resulting state" val === newState -- | Makes a validator for `testIndigo` that expects the stack to have 3 element -- (in order and with given values) or a failure to occur. validateStack2 :: forall m st pm . (MonadTest m, Eq pm, Eq st, Show pm, Show st) => (pm -> st -> Either MichelsonFailed (pm, st)) -> IndigoInstrValidator m pm st '[pm, st] validateStack2 fn param st res = assertMichelsonResult (fn param st) res checkSt where checkSt :: (pm, st) -> (Rec Identity '[pm, st]) -> m () checkSt val resStack = do let Identity newParam :& Identity newState :& RNil = resStack annotate "matches resulting state" val === (newParam, newState) -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- assertMichelsonResult :: MonadTest m => Either MichelsonFailed a -> Either MichelsonFailed b -> (a -> b -> m ()) -> m () assertMichelsonResult mRes1 mRes2 validatorRight = case (mRes1, mRes2) of (Left err, Left e) -> annotate "expected failure" >> err === e (Left err, Right _) -> failedTest $ "should have failed with: " <> pretty err (Right _, Left e) -> failedTest $ "unexpected failure: " <> pretty e (Right val1, Right val2) -> validatorRight val1 val2 noOptimizationContract :: ContractCode param st -> Contract param st noOptimizationContract code = Contract { cCode = code , cDisableInitialCast = False , cCompilationOptions = noOptimizationOptions } noOptimizationOptions :: CompilationOptions noOptimizationOptions = defaultCompilationOptions { coOptimizerConf = Nothing }