cleveland-0.1.1: morley-test/Test/Interpreter.hs
-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
-- TODO [#712]: Remove this next major release
{-# OPTIONS_GHC -Wno-deprecations #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.Interpreter
( test_basic5
, test_increment
, test_fail
, test_mutez_add_overflow
, test_mutez_sub_underflow
, test_basic1
, test_lsl
, test_lsr
, test_FAILWITH
, test_gas_exhaustion
, test_add1_list
, test_Sum_types
, test_Product_types
, test_split_bytes
, test_split_string_simple
, test_complex_strings
, test_contract_instr_on_implicit
, test_map_preserve_stack
, test_AND_binary
, test_EDIV
, test_SELF_address_packing
, test_fail_callstack
) where
import Fmt ((+|), (|+))
import Hedgehog (forAll, property)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog (testProperty)
import Lorentz (HasEntrypointArg, NiceParameter, NiceParameterFull, NiceStorage)
import Morley.Michelson.ErrorPos (InstrCallStack(..), LetName(..), srcPos)
import Morley.Michelson.Text
import Morley.Michelson.Typed (IsoValue(..), divMich, modMich)
import Morley.Michelson.Typed qualified as T
import Morley.Michelson.Untyped qualified as U
import Morley.Tezos.Address
import Morley.Tezos.Crypto
import Test.Cleveland
import Test.Cleveland.Internal.Abstract (TransferFailure(..), TransferFailureReason(..))
import Test.Cleveland.Michelson (concatTestTrees, testTreesWithTypedContract)
import Test.Cleveland.Michelson.Import
(testTreesWithTypedContractExt, testTreesWithUntypedContractExt)
import Test.Util.Contracts
catchTransferFailureReason :: (HasCallStack, MonadCleveland caps m) => m a -> m TransferFailureReason
catchTransferFailureReason = fmap tfReason . catchTransferFailure
callSimple
:: forall cp st caps m.
( HasCallStack
, MonadCleveland caps m
, HasEntrypointArg cp (EntrypointRef 'Nothing) cp
, AsRPC st ~ st
, NiceParameterFull cp, NiceStorage st
)
=> AliasHint -> T.Contract (ToT cp) (ToT st) -> cp -> st -> m st
callSimple nm contract cp st = do
handle <- originateTypedSimple @cp @st @() nm st contract
call handle CallDefault cp
getStorage handle
callUntyped
:: forall cp st caps m.
( MonadCleveland caps m
, T.ConstantScope (ToT st)
, AsRPC st ~ st
, NiceStorage st, NiceParameter cp
)
=> AliasHint -> U.Contract -> cp -> st -> m st
callUntyped nm contract cp st = do
handle <- originateUntypedSimple nm (T.untypeValue $ T.toVal st) contract
transfer TransferData
{ tdTo = handle
, tdAmount = 0
, tdEntrypoint = T.DefEpName
, tdParameter = cp
}
getStorage @st handle
test_basic5 :: IO [TestTree]
test_basic5 =
testTreesWithTypedContract (contractsDir </> "basic5.tz") $ \contract -> pure
[ testScenarioOnEmulator "Basic test" $ scenario $
callSimple "basic5" contract () [1] @@== [13, 100 :: Integer]
]
test_increment :: IO [TestTree]
test_increment =
testTreesWithTypedContract (contractsDir </> "increment.tz") $ \contract -> pure
[ testScenarioOnEmulator "Basic test" $ scenario $
callSimple "increment" contract () 23 @@== (24 :: Integer)
]
test_fail :: IO [TestTree]
test_fail =
testTreesWithTypedContract (contractsDir </> "tezos_examples/macros/fail.tz") $ \contract -> pure
[ testScenarioOnEmulator "Fail test" $ scenario $
callSimple "fail" contract () () & expectFailedWith ()
]
test_mutez_add_overflow :: IO [TestTree]
test_mutez_add_overflow =
testTreesWithTypedContract (contractsDir </> "mutez_add_overflow.tz") $ \contract -> pure
[ testScenarioOnEmulator "Mutez add overflow test" $ scenario $
callSimple "mutez_add_overflow" contract () ()
& catchTransferFailureReason >>= \case
MutezArithError T.AddOverflow -> pass
_ -> failure "expecting another failure reason"
]
test_mutez_sub_underflow :: IO [TestTree]
test_mutez_sub_underflow =
testTreesWithTypedContract (contractsDir </> "mutez_sub_underflow.tz") $ \contract -> pure
[ testScenarioOnEmulator "Mutez sub underflow test" $ scenario $
callSimple "mutez_sub_underflow" contract () ()
]
test_basic1 :: IO [TestTree]
test_basic1 =
testTreesWithTypedContract (contractsDir </> "basic1.tz") $ \contract -> pure
[ testProperty "Random check" $ property $ do
input <- forAll $ Gen.list (Range.linear 0 100) (Gen.integral (Range.linearFrom 0 -1000 1000))
testScenarioProps $ scenario $
callSimple "basic1" contract () input @@== [sum input + 12, 100 :: Integer]
]
test_lsl :: IO [TestTree]
test_lsl =
testTreesWithTypedContract (contractsDir </> "lsl.tz") $ \contract -> pure
[ testScenarioOnEmulator "LSL shouldn't overflow test" $ scenario $
callSimple @Natural @Natural "lsl" contract 5 2 @@== 20
, testScenarioOnEmulator "LSL should overflow test" $ scenario $
callSimple "lsl" contract (5 :: Natural) (257 :: Natural)
& expectTransferFailure shiftOverflow
]
test_lsr :: IO [TestTree]
test_lsr =
testTreesWithTypedContract (contractsDir </> "lsr.tz") $ \contract -> pure
[ testScenarioOnEmulator "LSR shouldn't underflow test" $ scenario $
callSimple @Natural @Natural "lsr" contract 30 3 @@== 3
, testScenarioOnEmulator "LSR should underflow test" $ scenario $
callSimple "lsr" contract (1000 :: Natural) (257 :: Natural)
& catchTransferFailureReason >>= \case
ShiftOverflow -> pass
x -> failure $ "expecting another failure reason, got " +| x |+ ""
]
test_FAILWITH :: IO [TestTree]
test_FAILWITH = concatTestTrees
[ testTreesWithTypedContract (contractsDir </> "failwith_message.tz") $ \contract ->
pure
[ testScenarioOnEmulator "Failwith message test" $ scenario $
let msg = "An error occurred." :: MText
in callSimple "failwith_message" contract msg ()
& expectFailedWith msg
]
, testTreesWithTypedContract (contractsDir </> "failwith_message2.tz") $ \contract ->
pure
[ testScenarioOnEmulator "Conditional failwith message test" $ scenario do
let msg = "An error occurred." :: MText
callSimple "failwith_message2" contract (True, msg) ()
& expectFailedWith msg
, testScenarioOnEmulator "Conditional success test" $ scenario do
let param = (False, "Err" :: MText)
callSimple "failwith_message2" contract param ()
]
]
test_gas_exhaustion :: IO [TestTree]
test_gas_exhaustion =
testTreesWithTypedContract (contractsDir </> "gas_exhaustion.tz") $ \contract -> pure
[ testScenarioOnEmulator "Contract should fail due to gas exhaustion" $ scenario $
let dummyStr = "x" :: MText
in callSimple "gas_exhaustion" contract dummyStr dummyStr
& catchTransferFailureReason >>= \case
GasExhaustion -> pass
e -> failure $ "Expected gas_exhaustion, but got " +| e |+ ""
]
test_fail_callstack :: IO [TestTree]
test_fail_callstack =
testTreesWithUntypedContractExt (contractsDir </> "fail_in_let.mtz") $ \contract -> pure
[ testScenarioOnEmulator "Should correctly report FAIL instruction position" $ scenario do
callUntyped "fail_in_let" contract True () & catchTransferFailureReason >>= \case
FailedWith _ (Just ics) ->
ics @== InstrCallStack {
icsCallStack = map LetName ["letfail1", "letfail2", "letfail3"]
, icsSrcPos = srcPos 5 29
}
err -> failure $
"Expected interpreter to fail with callstack, but it instead errored with " +|
err |+ ""
]
test_add1_list :: IO [TestTree]
test_add1_list =
testTreesWithTypedContract (contractsDir </> "tezos_examples/attic/add1_list.tz") $ \contract ->
pure
[ testProperty "Random check" $ property $ do
param <- forAll $ Gen.list (Range.linear 0 100) (Gen.integral (Range.linearFrom 0 -1000 1000))
testScenarioProps $ scenario $
callSimple @[Integer] @[Integer] "add1_list" contract param param @@== map succ param
]
test_Sum_types :: IO [TestTree]
test_Sum_types = concatTestTrees
[ testTreesWithUntypedContractExt (contractsDir </> "union.mtz") $ \contract -> pure
[ testGroup "union.mtz: union corresponds to Haskell types properly" $
let caseTest param = scenario $ callUntyped "union" contract param ()
in
[ testScenarioOnEmulator "Case 1" $ caseTest (Case1 3)
, testScenarioOnEmulator "Case 2" $ caseTest (Case2 "a")
, testScenarioOnEmulator "Case 3" $ caseTest (Case3 $ Just "b")
, testScenarioOnEmulator "Case 4" $ caseTest (Case4 $ Left "b")
, testScenarioOnEmulator "Case 5" $ caseTest (Case5 ["q"])
]
]
, testTreesWithUntypedContractExt (contractsDir </> "case.mtz") $ \contract -> pure
[ testGroup "CASE instruction" $
let caseTest param expectedStorage = scenario $
callUntyped "case" contract param ("" :: MText)
@@== expectedStorage
in
[ testScenarioOnEmulator "Case 1" $ caseTest (Case1 5) "int"
, testScenarioOnEmulator "Case 2" $ caseTest (Case2 "a") "string"
, testScenarioOnEmulator "Case 3" $ caseTest (Case3 $ Just "aa") "aa"
, testScenarioOnEmulator "Case 4" $ caseTest (Case4 $ Right "b") "or string string"
, testScenarioOnEmulator "Case 5" $ caseTest (Case5 $ ["a", "b"]) "ab"
]
]
, testTreesWithTypedContractExt (contractsDir </> "tag.mtz") $ \contract -> pure
[ testScenarioOnEmulator "TAG instruction" $ scenario $
let expected = mconcat ["unit" :: MText, "o" :: MText, "ab" :: MText, "nat" :: MText, "int" :: MText]
in callSimple "tag" contract () ("" :: MText) @@== expected
]
]
test_Product_types :: IO [TestTree]
test_Product_types = concatTestTrees
[ testTreesWithTypedContractExt (contractsDir </> "access.mtz") $ \contract -> pure
[ testScenarioOnEmulator "ACCESS instruction" $ scenario $
callSimple @Tuple1 "access" contract (1, "a", Just "a", Right "a", ["a"]) ()
]
, testTreesWithTypedContractExt (contractsDir </> "set.mtz") $ \contract -> pure
[ testScenarioOnEmulator "SET instruction" $ scenario $
let expected = (2, "za", Just "wa", Right "ya", ["ab"]) :: Tuple1
in callSimple @_ @Tuple1 "set" contract () (1, "a", Just "a", Right "a", ["a", "b"])
@@== expected
]
, testTreesWithTypedContractExt (contractsDir </> "construct.mtz") $ \contract -> pure
[ testScenarioOnEmulator "CONSTRUCT instruction" $ scenario $
let expected = (1, "a", Just "b", Left "q", []) :: Tuple1
in callSimple @_ @Tuple1 "construct" contract () (0, "", Nothing, Right "", []) @@== expected
]
]
test_split_bytes :: IO [TestTree]
test_split_bytes =
testTreesWithTypedContract (contractsDir </> "tezos_examples/opcodes/split_bytes.tz") $
\contract -> pure
[ testScenarioOnEmulator "splits given byte sequence into parts" $ scenario $
let expected = ["\11", "\12", "\13"] :: [ByteString]
in callSimple "split_bytes" contract ("\11\12\13" :: ByteString) ([] :: [ByteString])
@@== expected
]
test_split_string_simple :: IO [TestTree]
test_split_string_simple =
testTreesWithTypedContract (contractsDir </> "split_string_simple.tz") $ \contract ->
pure
[ testScenarioOnEmulator "applies SLICE instruction" $ scenario do
let
oneTest o l str expected =
callSimple @(Natural, Natural) @(Maybe MText) "split_string_simple"
contract (o, l) (Just str) @@== expected
-- These values have been tested using tezos-client
oneTest 0 0 "aaa" (Just "")
oneTest 2 0 "aaa" (Just "")
oneTest 3 0 "aaa" Nothing
oneTest 0 5 "aaa" Nothing
oneTest 1 2 "abc" (Just "bc")
oneTest 1 1 "abc" (Just "b")
oneTest 2 1 "abc" (Just "c")
oneTest 2 2 "abc" Nothing
oneTest 1 1 "\"\"" (Just "\"")
oneTest 1 2 "a\n" Nothing
]
test_complex_strings :: IO [TestTree]
test_complex_strings =
testTreesWithTypedContract (contractsDir </> "complex_strings.tz") $ \contract ->
pure
[ testScenarioOnEmulator "ComplexString" $ scenario $
callSimple "complex_strings" contract ("text: " :: MText) ("" :: MText)
@@== ("text: \"aa\" \\\n" :: MText)
]
data Union1
= Case1 Integer
| Case2 MText
| Case3 (Maybe MText)
| Case4 (Either MText MText)
| Case5 [MText]
deriving stock (Generic)
deriving anyclass (IsoValue)
type Tuple1 = (Integer, MText, Maybe MText, Either MText MText, [MText])
test_contract_instr_on_implicit :: IO [TestTree]
test_contract_instr_on_implicit =
testTreesWithTypedContract (contractsDir </> "contract_instr_unit.tz") $ \contractGood ->
testTreesWithTypedContract (contractsDir </> "contract_instr_nonunit.tz") $ \contractBad ->
pure
[ testScenarioOnEmulator "CONTRACT instruction succeeds on implicit accounts" $ scenario $
callSimple "contract_instr_unit" contractGood addr ()
, testScenarioOnEmulator "CONTRACT instruction considers implicit accounts as unit-parametrized" $
scenario $ callSimple "contract_instr_nounit" contractBad addr ()
& expectFailedWith ("No such contract" :: MText)
]
where
addr = mkKeyAddress . toPublic $ detSecretKey "sfsdfsdf"
-- | This test creates a map of two items and then converts them into a list
-- with @MAP@ primitive, counting the number of items along the way.
--
-- See https://gitlab.com/morley-framework/morley/-/issues/123
test_map_preserve_stack :: IO [TestTree]
test_map_preserve_stack =
testTreesWithTypedContract (contractsDir </> "map_preserve_stack.tz") $ \contract ->
pure
[ testScenarioOnEmulator "MAP preserves deep stack modifications (#123)" $ scenario $
callSimple @() @([Integer], Integer) "map_preserve_stack" contract () ([], 0)
@@== ([257, 43], 2)
]
test_AND_binary :: IO [TestTree]
test_AND_binary =
testTreesWithTypedContract (contractsDir </> "tezos_examples/opcodes/and_binary.tz") $ \contract ->
pure
[ testScenarioOnEmulator "Binary AND test" $ scenario $
callSimple "and_binary" contract () ()
]
type TestEdivStorage =
( Maybe (Integer, Natural)
, ( Maybe (Integer, Natural)
, ( Maybe (Integer, Natural)
, Maybe (Natural, Natural)
)
)
)
test_EDIV :: IO [TestTree]
test_EDIV =
testTreesWithTypedContract (contractsDir </> "tezos_examples/opcodes/ediv.tz") $ \contract ->
pure
[ testScenarioOnEmulator "EDIV of int and nat test (non-zero modulo)" $ scenario $
let intNatNeg = (278 `divMich` -167, fromInteger $ (278 `modMich` -167))
intNatPos = (278 `divMich` 167, 278 `modMich` 167)
natNat = (278 `divMich` 167, 278 `modMich` 167)
in callSimple @(Integer, Integer) @TestEdivStorage "ediv" contract
(278, -167) (Nothing, (Nothing, (Nothing, Nothing)))
@@== (Just intNatNeg, (Just intNatPos, (Just intNatNeg, Just natNat)))
, testScenarioOnEmulator "EDIV of int and nat test (zero modulo)" $ scenario $
callSimple @(Integer, Integer) @TestEdivStorage "ediv" contract
(109, -1) (Nothing, (Nothing, (Nothing, Nothing)))
@@== (Just (-109, 0), (Just (109, 0), (Just (-109, 0), Just (109, 0))))
]
test_SELF_address_packing :: IO [TestTree]
test_SELF_address_packing =
testTreesWithTypedContract (contractsDir </> "entrypoints/self_pack1.tz") $ \contract ->
pure
[ testScenarioOnEmulator "SELF address packing (#333)" $ scenario $
callSimple "self_pack1" contract () ()
]