-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA -- | Tests for 'Morley.Micheline.Expression'. module Test.Morley.Expression ( test_Roundtrip_binary , test_Roundtrip_expression , test_Roundtrip_JSON , test_ToJSON_omits_empty_lists , test_fromExpression , test_toExpression ) where import Unsafe qualified (fromJust) import Data.Aeson (FromJSON, Result(Error, Success), ToJSON(toJSON), Value(String), eitherDecode, encode, fromJSON, object, (.=)) import Data.Aeson.QQ (aesonQQ) import Data.Default (def) import Data.Map qualified as Map import Data.Set qualified as Set import Data.Singletons (demote) import Data.Text qualified as T import Hedgehog (Gen) import Test.Hspec.Expectations (shouldBe) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (Assertion, assertFailure, testCase, (@?=)) import Hedgehog.Gen.Michelson.Typed (genSimpleInstr, genValueInt, genValueMutez, genValueUnit) import Hedgehog.Gen.Michelson.Untyped (genValidType) import Hedgehog.Gen.Morley.Micheline qualified as M import Morley.Micheline qualified as M import Morley.Michelson.Parser (notes) import Morley.Michelson.Text (MText) import Morley.Michelson.Typed qualified as T import Morley.Michelson.Untyped qualified as U import Morley.Tezos.Core (Mutez) import Test.Cleveland.Instances () import Test.Cleveland.Util (fromHex) import Test.Util.Hedgehog (roundtripTree) testJSON :: forall a. (Eq a, Show a, ToJSON a, FromJSON a, Typeable a) => Gen a -> TestTree testJSON genA = roundtripTree @a genA encode eitherDecode -- | Test that 'M.fromExpression' is inverse of 'M.toExpression' roundtripExpression :: forall a. (Eq a, Show a, M.ToExpression a, M.FromExpression a, Typeable a) => Gen a -> TestTree roundtripExpression genA = roundtripTree @a genA M.toExpression (M.fromExpression) -- | Test that converting something to 'M.Expression', then to JSON, -- then decoding this JSON and converting back to the original type -- returns the original value. roundtripExpressionJSON :: forall a. (Eq a, Show a, M.ToExpression a, M.FromExpression a, Typeable a) => Gen a -> TestTree roundtripExpressionJSON genA = roundtripTree @a genA (encode . M.toExpression) (first displayException . M.fromExpression <=< eitherDecode) testBinary :: Gen M.Expression -> TestTree testBinary genA = roundtripTree genA M.encodeExpression' M.eitherDecodeExpression test_Roundtrip_JSON :: [TestTree] test_Roundtrip_JSON = [ testJSON M.genExprAnnotation , testJSON M.genExpression , testJSON M.genMichelinePrimAp , roundtripExpressionJSON @(T.Value $ T.ToT Integer) genValueInt , roundtripExpressionJSON @(T.Value $ T.ToT Mutez) genValueMutez , roundtripExpressionJSON @(T.Value $ T.ToT ()) genValueUnit , roundtripExpressionJSON @(T.Instr '[T.ToT Mutez] '[T.ToT Mutez]) genSimpleInstr , roundtripExpressionJSON (T.fromUType <$> genValidType) , roundtripExpressionJSON genValidType ] test_ToJSON_omits_empty_lists :: TestTree test_ToJSON_omits_empty_lists = testCase "ToJSON omits empty lists" $ do let actual = toJSON . M.toExpression $ T.toVal (Nothing :: Maybe Integer) let expected = object ["prim" .= String "None"] actual @?= expected test_Roundtrip_binary :: [TestTree] test_Roundtrip_binary = [ testBinary M.genExpression ] test_Roundtrip_expression :: [TestTree] test_Roundtrip_expression = [ roundtripExpression @(T.Value $ T.ToT Integer) genValueInt , roundtripExpression @(T.Value $ T.ToT Mutez) genValueMutez , roundtripExpression @(T.Value $ T.ToT ()) genValueUnit , roundtripExpression @(T.Instr '[T.ToT Mutez] '[T.ToT Mutez]) genSimpleInstr , roundtripExpression @T.T (T.fromUType <$> genValidType) , roundtripExpression genValidType ] test_fromExpression :: [TestTree] test_fromExpression = -- These michelson expressions are generated using commands like the following: -- -- > tezos-client convert data 'Pair 1 2 "Hi"' from michelson to json -- > tezos-client convert data 'pair int nat string' from michelson to json [ testGroup "Value t" [ testCase "Converting 'Pair' expression with >2 args into right-combed pair" $ do fromExpressionTest (T.toVal @(Integer, (Natural, MText)) (1, (2, "Hi"))) [aesonQQ| { "prim": "Pair", "args": [ { "int": "1" }, { "int": "2" }, { "string": "Hi" } ] } |] , testCase "Converting nested 'Pair' expression into right-combed pair" $ do fromExpressionTest (T.toVal @(Integer, (Natural, MText)) (1, (2, "Hi"))) [aesonQQ| { "prim": "Pair", "args": [ { "int": "1" }, { "prim": "Pair", "args": [ { "int": "2" }, { "string": "Hi" } ] } ] } |] , testCase "Converting list expression into right-combed pair" $ do fromExpressionTest (T.toVal @(Integer, (Natural, MText)) (1, (2, "Hi"))) [aesonQQ| [ { "int": "1" }, { "int": "2" }, { "string": "Hi" } ] |] ] , testGroup "Type" [ testCase "Converting 'pair' type expression with >2 args into right-combed pair type" $ do fromExpressionTest (T.toUType $ demote @(T.ToT (Integer, (Natural, MText)))) [aesonQQ| { "prim": "pair", "args": [ { "prim": "int" }, { "prim": "nat" }, { "prim": "string" } ] } |] , testCase "Converting nested 'pair' type expression into right-combed pair type" $ do fromExpressionTest (T.toUType $ demote @(T.ToT (Integer, (Natural, MText)))) [aesonQQ| { "prim": "pair", "args": [ { "prim": "int" }, { "prim": "pair", "args": [ { "prim": "nat" }, { "prim": "string" } ] } ] } |] ] , testGroup "Contract" [ testCase "Conversion of contract with duplicated 'storage' block fails" $ do fromExpressionFailWithPredicateTest @U.Contract (T.isInfixOf "Something's wrong with top-level contract blocks") [aesonQQ| [ { "prim": "storage", "args": [ { "prim": "unit" } ] }, { "prim": "parameter", "args": [ { "prim": "unit" } ] }, { "prim": "storage", "args": [ { "prim": "unit" } ] } ] |] , testCase "Conversion of contract with absence one of blocks fails" $ do fromExpressionFailWithPredicateTest @U.Contract (T.isInfixOf "Something's wrong with top-level contract blocks") [aesonQQ| [ { "prim": "storage", "args": [ { "prim": "unit" } ] }, { "prim": "storage", "args": [ { "prim": "unit" } ] }, { "prim": "code", "args": [ [ { "prim": "DROP" }, { "prim": "UNIT" }, { "prim": "NIL", "args": [ { "prim": "operation" } ] }, { "prim": "PAIR" } ] ] } ] |] , testCase "Conversion of contract with invalid block arguments count fails" $ do fromExpressionFailWithPredicateTest @U.Contract (T.isInfixOf "Expected 'storage' block without annotations and exactly 1 argument") [aesonQQ| [ { "prim": "storage", "args": [ { "prim": "unit"}, {"prim": "int"} ] }, { "prim": "parameter", "args": [ { "prim": "unit" } ] }, { "prim": "code", "args": [ [ { "prim": "DROP" }, { "prim": "UNIT" }, { "prim": "NIL", "args": [ { "prim": "operation" } ] }, { "prim": "PAIR" } ] ] } ] |] , testCase "Conversion of contract with error inside block fails" $ do fromExpressionFailWithPredicateTest @U.Contract (T.isInfixOf "Expected a type") [aesonQQ| [ { "prim": "storage", "args": [ { "prim": "unit", "args": [ { "prim": "int" } ] } ] }, { "prim": "parameter", "args": [ { "prim": "unit" } ] }, { "prim": "code", "args": [ [ { "prim": "DROP" }, { "prim": "UNIT" }, { "prim": "NIL", "args": [ { "prim": "operation" } ] }, { "prim": "PAIR" } ] ] } ] |] , testCase "Conversion of contract with parameter which contains more than 1 root ann" $ do fromExpressionFailWithPredicateTest @U.Contract (T.isInfixOf "Expected parameter with at most 1 root annotation") [aesonQQ| [ { "prim": "storage", "args": [ { "prim": "unit" } ] }, { "prim": "parameter", "args": [ { "prim": "unit", "annots": [ "%root1", "%root2" ] } ]}, { "prim": "code", "args": [ [ { "prim": "DROP" }, { "prim": "UNIT" }, { "prim": "NIL", "args": [ { "prim": "operation" } ] }, { "prim": "PAIR" } ] ] } ] |] ] , testGroup "Annotations" [ testGroup "Instructions" [ testCase "Conversion of instruction with extra annotations fails" $ do fromExpressionFailTest @U.ExpandedInstr [aesonQQ| { "prim": "PAIR", "annots": [ ":ta", "@va1", "@va2", "%fa1", "%fa2", "%fa3" ] } |] , testCase "Conversion of instruction with annotation of unexpected type fails" $ do fromExpressionFailTest @U.ExpandedInstr [aesonQQ| { "prim": "DUP", "annots": [ "@va", "%fa" ] } |] ] , testGroup "Types" [ testCase "Conversion of type with extra annotations fails" $ do fromExpressionFailTest @U.Ty [aesonQQ| { "prim": "int", "annots": [ ":ta", "%fa" ] } |] ] ] ] where fromExpressionTest :: forall t. (Eq t, Show t, M.FromExpression t) => t -> Value -> Assertion fromExpressionTest expected exprJSON = case fromJSON @M.Expression exprJSON of Error err -> assertFailure err Success expr -> do actual <- either (assertFailure . displayException) pure $ M.fromExpression @t expr actual @?= expected fromExpressionFailTest :: forall t. (M.FromExpression t) => Value -> Assertion fromExpressionFailTest = fromExpressionFailWithPredicateTest @t (const True) fromExpressionFailWithPredicateTest :: forall t. (HasCallStack, M.FromExpression t) => (Text -> Bool) -> Value -> Assertion fromExpressionFailWithPredicateTest p exprJSON = case fromJSON @M.Expression exprJSON of Error err -> assertFailure err Success expr -> case M.fromExpression @t expr of Right _ -> assertFailure "Test was expected to fail, but it passed" Left (M.FromExpressionError _ msg) -> unless (p msg) $ assertFailure $ "Test failed with an unexpected error type:\n " <> toString msg test_toExpression :: [TestTree] test_toExpression = [ testCase "Converting TInt to Expression" $ M.toExpression T.TInt `shouldBe` M.ExpressionPrim (M.MichelinePrimAp (M.MichelinePrimitive "int") [] []) , testCase "Converting (TOption TString) to Expression" $ M.toExpression (T.TOption T.TString) `shouldBe` M.ExpressionPrim ( M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "option" , mpaArgs = [M.ExpressionPrim ( M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "string" , mpaArgs = [], mpaAnnots = [] } )] , mpaAnnots = [] } ) , testCase "Converting Instr (DIP SIZE) to Expression" $ M.toExpression (T.DIP (T.SIZE @'T.TString)) `shouldBe` M.ExpressionSeq [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "DIP" , mpaArgs = [M.ExpressionSeq [M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "SIZE" , mpaArgs = [] , mpaAnnots = [] })]] , mpaAnnots = [] })] , testCase "Converting VInt to Expression" $ M.toExpression @(T.Value $ T.ToT Integer) (T.VInt 12) `shouldBe` M.ExpressionInt 12 , testCase "Converting VPair to Expression" $ M.toExpression @(T.Value $ T.ToT (Integer, Integer)) (T.VPair (T.VInt 12, T.VInt 12)) `shouldBe` M.ExpressionSeq [M.ExpressionInt 12, M.ExpressionInt 12] , testCase "Converting VOption (Some) to Expression" $ M.toExpression @(T.Value $ T.ToT (Maybe Integer)) (T.VOption $ Just $ T.VInt 42) `shouldBe` M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Some" , mpaArgs = [M.ExpressionInt 42] , mpaAnnots = [] }) , testCase "Converting VOption (None) to Expression" $ M.toExpression @(T.Value $ T.ToT (Maybe Integer)) (T.VOption Nothing) `shouldBe` M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "None" , mpaArgs = [] , mpaAnnots = [] }) , testCase "Converting VList to Expression" $ M.toExpression @(T.Value $ T.ToT [Integer]) (T.VList [T.VInt 12, T.VInt 12]) `shouldBe` M.ExpressionSeq [M.ExpressionInt 12, M.ExpressionInt 12] , testCase "Converting VMap to Expression" $ M.toExpression @(T.Value $ T.ToT (Map Integer Integer)) (T.VMap (Map.fromList [(T.VInt 12, T.VInt 12), (T.VInt 13, T.VInt 13)])) `shouldBe` M.ExpressionSeq [ (M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Elt" , mpaArgs = [M.ExpressionInt 12, M.ExpressionInt 12] , mpaAnnots = [] } ) , (M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Elt" , mpaArgs = [M.ExpressionInt 13, M.ExpressionInt 13] , mpaAnnots = [] } ) ] , testCase "Converting VOr (Right) to Expression" $ M.toExpression @(T.Value $ T.ToT (Either Integer Integer)) (T.VOr $ Right $ T.VInt 12) `shouldBe` M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Right" , mpaArgs = [M.ExpressionInt 12] , mpaAnnots = [] } , testCase "Converting VOr (Left) to Expression" $ M.toExpression @(T.Value $ T.ToT (Either Integer Integer)) (T.VOr $ Left $ T.VInt 12) `shouldBe` M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Left" , mpaArgs = [M.ExpressionInt 12] , mpaAnnots = [] } , testCase "Converting VSet to Expression" $ M.toExpression @(T.Value $ T.ToT (Set Integer)) (T.VSet $ Set.fromList [T.VInt 1, T.VInt 2]) `shouldBe` M.ExpressionSeq [M.ExpressionInt 1, M.ExpressionInt 2] , testCase "Converting VMutez to Expression" $ M.toExpression @(T.Value $ T.ToT Mutez) (T.VMutez $ 12) `shouldBe` M.ExpressionInt 12 , testCase "Converting VBool to Expression" $ M.toExpression @(T.Value $ T.ToT Bool) (T.VBool True) `shouldBe` M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "True" , mpaArgs = [] , mpaAnnots = [] }) , testCase "Converting VContract to Expression" $ M.toExpression @(T.Value $ 'T.TContract 'T.TAddress) (getSampleValue @('T.TContract 'T.TAddress)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "01122d038abd69be91b4b6803f2f098a088e259e7200") , testCase "Converting VBigMap to Expression" $ M.toExpression @(T.Value $ 'T.TBigMap (T.ToT Integer) (T.ToT Integer)) (T.VBigMap Nothing $ Map.fromList [(T.VInt 1, T.VInt 2), (T.VInt 3, T.VInt 4)]) `shouldBe` M.ExpressionSeq [ (M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Elt" , mpaArgs = [M.ExpressionInt 1, M.ExpressionInt 2] , mpaAnnots = [] } ) , (M.ExpressionPrim M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Elt" , mpaArgs = [M.ExpressionInt 3, M.ExpressionInt 4] , mpaAnnots = [] } ) ] , testCase "Converting VUnit to Expression" $ M.toExpression @(T.Value 'T.TUnit) T.VUnit `shouldBe` M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Unit" , mpaArgs = [] , mpaAnnots = [] }) , testCase "Converting VKey to Expression" $ M.toExpression @(T.Value 'T.TKey) (getSampleValue @('T.TKey)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "00aad3f16293766169f7db278c5e0e9db4fb82ffe1cbcc35258059617dc0fec082") , testCase "Converting VTimestamp to Expression" $ M.toExpression @(T.Value 'T.TTimestamp) (getSampleValue @('T.TTimestamp)) `shouldBe` M.ExpressionInt 1564142952 , testCase "Converting VAddress to Expression" $ M.toExpression @(T.Value 'T.TAddress) (getSampleValue @('T.TAddress)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "01122d038abd69be91b4b6803f2f098a088e259e7200") , testCase "Converting VChainId to Expression" $ M.toExpression @(T.Value 'T.TChainId) (getSampleValue @('T.TChainId)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "458aa837") , testCase "Converting VSignature to Expression" $ M.toExpression @(T.Value 'T.TSignature) (getSampleValue @('T.TSignature)) `shouldBe` (M.ExpressionBytes $ unsafe $ fromHex "91ac1e7fd668854fc7a40feec4034e42c06c068cce10622c607fda232db34c8cf5d8da83098dd89\ \1cd4cb4299b3fa0352ae323ad99b24541e54b91888fdc8201") , testCase "Converting VKeyHash to Expression" $ M.toExpression @(T.Value 'T.TKeyHash) (getSampleValue @('T.TKeyHash)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "0092629ed0afa9cd42835ce09ee2623c1efa0b590d") , testCase "Converting VBls12381Fr to Expression" $ M.toExpression @(T.Value 'T.TBls12381Fr) (getSampleValue @('T.TBls12381Fr)) `shouldBe` M.ExpressionBytes (unsafe $ fromHex "0100000000000000000000000000000000000000000000000000000000000000") , testCase "Converting VLam to Expression" $ M.toExpression @(T.Value $ 'T.TLambda 'T.TUnit 'T.TUnit) (getSampleValue @('T.TLambda 'T.TUnit 'T.TUnit)) `shouldBe` M.ExpressionSeq [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "DROP" , mpaArgs = [] , mpaAnnots = [] } ) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "PUSH" , mpaArgs = [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "unit" , mpaArgs = [] , mpaAnnots = [] }) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "Unit" , mpaArgs = [] , mpaAnnots = [] }) ] , mpaAnnots = [] }) ] , testCase "Converting NTPair to Expression" $ M.toExpression [notes|pair :pair (string %a) (int %b :val)|] `shouldBe` M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "pair" , mpaArgs = [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "string" , mpaArgs = [] , mpaAnnots = [M.AnnotationField "a"] }) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "int" , mpaArgs = [] , mpaAnnots = [ M.AnnotationType "val", M.AnnotationField "b"] }) ] , mpaAnnots = [ M.AnnotationType "pair" ] } ) , testCase "Converting \"CAR @bar %foo\" to Expression" $ M.toExpression (T.AnnCAR "bar" "foo") `shouldBe` M.ExpressionSeq [ M.ExpressionPrim $ M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "CAR" , mpaArgs = [] , mpaAnnots = [ M.AnnotationField "foo" , M.AnnotationVariable "bar" ] } ] , testCase "Converting sample contract to Expression" $ M.toExpression sampleContract `shouldBe` M.ExpressionSeq [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "storage" , mpaArgs = [M.ExpressionPrim (M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "chain_id", mpaArgs = [], mpaAnnots = []})] , mpaAnnots = [] }) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "parameter" , mpaArgs = [M.ExpressionPrim (M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "unit", mpaArgs = [], mpaAnnots = []})] , mpaAnnots = [] }) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "code" , mpaArgs = [M.ExpressionSeq [ M.ExpressionPrim (M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "DROP", mpaArgs = [], mpaAnnots = []}) , M.ExpressionPrim (M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "CHAIN_ID", mpaArgs = [], mpaAnnots = []}) , M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "NIL" , mpaArgs = [ M.ExpressionPrim (M.MichelinePrimAp { mpaPrim = M.MichelinePrimitive "operation" , mpaArgs = [] , mpaAnnots = [] }) ] , mpaAnnots = [] }) , M.ExpressionPrim (M.MichelinePrimAp {mpaPrim = M.MichelinePrimitive "PAIR", mpaArgs = [], mpaAnnots = []}) ]] , mpaAnnots = [] }) ] ] where sampleContract :: T.Contract 'T.TUnit 'T.TChainId sampleContract = let code = T.DROP `T.Seq` T.CHAIN_ID `T.Seq` T.NIL `T.Seq` T.PAIR in T.Contract { cCode = code , cParamNotes = T.starParamNotes @'T.TUnit , cStoreNotes = T.starNotes , cEntriesOrder = U.SPC , cViews = def } getSampleValue :: forall t. (T.SingI t) => T.Value t getSampleValue = Unsafe.fromJust $ T.sampleTypedValue $ T.sing @t