cleveland-0.1.0: morley-test/Test/Morley/Expression.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- | 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 Data.Aeson
(FromJSON, Result(Error, Success), ToJSON(toJSON), Value(String), eitherDecode, encode, fromJSON,
object, (.=))
import Data.Aeson.QQ (aesonQQ)
import Data.Default (def)
import qualified Data.Map as Map
import Data.Maybe (fromJust)
import qualified Data.Set as Set
import Data.Singletons (demote)
import qualified Data.Text 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 qualified Hedgehog.Gen.Morley.Micheline as M
import qualified Morley.Micheline as M
import Morley.Michelson.Text (MText)
import qualified Morley.Michelson.Typed as T
import qualified Morley.Michelson.Untyped as U
import Morley.Tezos.Core (Mutez)
import Test.Cleveland.Instances ()
import Test.Cleveland.Util (unsafeFromHex)
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 (unsafeFromHex "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 (unsafeFromHex "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 (unsafeFromHex "01122d038abd69be91b4b6803f2f098a088e259e7200")
, testCase "Converting VChainId to Expression" $
M.toExpression @(T.Value 'T.TChainId) (getSampleValue @('T.TChainId)) `shouldBe`
M.ExpressionBytes (unsafeFromHex "458aa837")
, testCase "Converting VSignature to Expression" $
M.toExpression @(T.Value 'T.TSignature) (getSampleValue @('T.TSignature)) `shouldBe`
(M.ExpressionBytes $ unsafeFromHex
"91ac1e7fd668854fc7a40feec4034e42c06c068cce10622c607fda232db34c8cf5d8da83098dd89\
\1cd4cb4299b3fa0352ae323ad99b24541e54b91888fdc8201")
, testCase "Converting VKeyHash to Expression" $
M.toExpression @(T.Value 'T.TKeyHash) (getSampleValue @('T.TKeyHash)) `shouldBe`
M.ExpressionBytes (unsafeFromHex "0092629ed0afa9cd42835ce09ee2623c1efa0b590d")
, testCase "Converting VBls12381Fr to Expression" $
M.toExpression @(T.Value 'T.TBls12381Fr) (getSampleValue @('T.TBls12381Fr)) `shouldBe`
M.ExpressionBytes (unsafeFromHex "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 (T.NTPair "pair" "a" "b" U.noAnn U.noAnn (T.NTString U.noAnn) (T.NTInt "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 = fromJust $ T.sampleTypedValue $ T.sing @t