Skip to content

Commit

Permalink
Merge pull request #152 from input-output-hk/hrajchert/scp-4697-rando…
Browse files Browse the repository at this point in the history
…m-token-and-party

SCP-4697 Implement GenerateRandomValue request/response
  • Loading branch information
hrajchert authored Dec 9, 2022
2 parents 9910090 + 63bfaf7 commit c8c67ad
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 54 deletions.
1 change: 0 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@
nixConfig = {
extra-substituters = [
"https://cache.iog.io"
"https://hydra.iohk.io"
];
extra-trusted-public-keys = [
"hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ="
Expand Down
3 changes: 3 additions & 0 deletions marlowe-spec-test/marlowe-spec-test.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ library
Marlowe.Spec
Marlowe.Spec.ClientProcess
Marlowe.Spec.Core
Marlowe.Spec.Core.Arbitrary
Marlowe.Spec.Core.Examples
Marlowe.Spec.Core.Examples.Swap
Marlowe.Spec.Core.Serialization.Json
Expand All @@ -48,6 +49,8 @@ library
process,
text,
optparse-applicative,
QuickCheck-GenT,
QuickCheck

executable marlowe-spec
import: lang
Expand Down
57 changes: 57 additions & 0 deletions marlowe-spec-test/src/Marlowe/Spec/Core/Arbitrary.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleInstances #-}

module Marlowe.Spec.Core.Arbitrary where

import SemanticsTypes (Token(..), Party)
import QuickCheck.GenT (GenT, frequency)
import Marlowe.Spec.Interpret (InterpretJsonRequest, Request (..), parseValidResponse)
import Data.Data (Proxy(..))
import Marlowe.Spec.TypeId (TypeId(..))
import Control.Monad.IO.Class (liftIO)
import Control.Exception (throwIO, Exception)
import Data.Aeson (FromJSON(..), withObject, (.:), (.=))
import Control.Applicative ((<|>))
import Data.Aeson (ToJSON (..), object)

data RandomResponse a
= RandomValue a
| UnknownType TypeId


instance ToJSON a => ToJSON (RandomResponse a) where
toJSON (RandomValue v) = object
[ "value" .= v
]
toJSON (UnknownType t) = object
[ "unknown-type" .= toJSON t
]

instance FromJSON a => FromJSON (RandomResponse a) where
parseJSON = withObject "RandomResponse" $
\v -> asRandomValue v <|> asUnknownType v
where
asRandomValue v = RandomValue <$> v .: "value"
asUnknownType v = UnknownType <$> v .: "unknown-type"

data GenerateRandomValueException = GenerateRandomValueException String
deriving (Show, Exception)

arbitraryToken :: InterpretJsonRequest -> GenT IO Token
arbitraryToken interpret = liftIO do
res <- interpret (GenerateRandomValue $ TypeId "Core.Token" (Proxy :: Proxy Token))
case parseValidResponse res of
Left err -> throwIO $ GenerateRandomValueException err
Right (UnknownType _) -> throwIO $ GenerateRandomValueException "Client process doesn't know how to generate Core.Token"
Right (RandomValue t) -> pure t


arbitraryParty :: InterpretJsonRequest -> GenT IO Party
arbitraryParty interpret = liftIO do
res <- interpret (GenerateRandomValue $ TypeId "Core.Party" (Proxy :: Proxy Party))
case parseValidResponse res of
Left err -> throwIO $ GenerateRandomValueException err
Right (UnknownType _) -> throwIO $ GenerateRandomValueException "Client process doesn't know how to generate Core.Party"
Right (RandomValue t) -> pure t
98 changes: 58 additions & 40 deletions marlowe-spec-test/src/Marlowe/Spec/Core/Serialization/Json.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Marlowe.Spec.Core.Serialization.Json where

import Control.Applicative ((<|>))
import Control.Monad (join)
import Data.Aeson.Types (Result(..), ToJSON(..), FromJSON(..))
import Data.Aeson (object, (.=), (.:), withObject)
import qualified Data.Aeson.Types as JSON
Expand All @@ -14,11 +16,13 @@ import Data.Proxy (Proxy(..))
import MarloweCoreJson
import GHC.Stack (HasCallStack)
import Test.Tasty (TestTree, testGroup)
import Marlowe.Spec.Interpret (Response (..), InterpretJsonRequest, exactMatch, Request (..), testResponse)
import Marlowe.Spec.Interpret (Response (..), InterpretJsonRequest, Request (..), testResponse)
import Marlowe.Spec.TypeId (TypeId(..), HasTypeId (..))
import Test.Tasty.Providers (TestName)
import Test.Tasty.HUnit (Assertion, assertBool)
import Test.Tasty.HUnit (Assertion, assertBool, testCase, (@?=))
import qualified SemanticsTypes as C
import QuickCheck.GenT (runGenT)
import Marlowe.Spec.Core.Arbitrary (arbitraryToken, arbitraryParty)
import Test.QuickCheck (generate)


data SerializationResponse transport
Expand Down Expand Up @@ -46,71 +50,85 @@ instance FromJSON (SerializationResponse JSON.Value) where
asUnknownType v = UnknownType <$> v .: "unknown-type"
asError v = SerializationError <$> v .: "serialization-error"

localJsonRoundtripSerialization :: TypeId -> JSON.Value -> SerializationResponse JSON.Value
localJsonRoundtripSerialization (TypeId _ p) v = roundtrip p
where
roundtrip :: forall a. ToJSON a => FromJSON a => Proxy a -> SerializationResponse JSON.Value
roundtrip _ = case JSON.fromJSON v :: Result a of
Error str -> SerializationError str
Success c -> SerializationSuccess $ JSON.toJSON c


tests :: InterpretJsonRequest -> TestTree
tests i = testGroup "Json Serialization"
[ roundtripExampleTest i "Bound example" condExample
[ testCase "Bound example" $ roundtripTest i condExample
, valueTests i
, observationTests i
, invalidType i
, tokenTest i
, partyTest i
]

valueTests :: InterpretJsonRequest -> TestTree
valueTests i = testGroup "Value examples"
[ roundtripExampleTest i "Constant" constantExample
, roundtripExampleTest i "Interval start" intervalStartExample
, roundtripExampleTest i "Interval end" intervalEndExample
, roundtripExampleTest i "Add" addExample
, roundtripExampleTest i "Sub" subExample
, roundtripExampleTest i "Mul" mulExample
, roundtripExampleTest i "Div" divExample
, roundtripExampleTest i "Negate" negateExample
-- , roundtripExampleTest i "Choice value" choiceValueExample
, roundtripExampleTest i "Use" useValueExample
, roundtripExampleTest i "Cond" condExample
-- ,roundtripExampleTest i "Available money" availableMoneyExample
[ testCase "Constant" $ roundtripTest i constantExample
, testCase "Interval start" $ roundtripTest i intervalStartExample
, testCase "Interval end" $ roundtripTest i intervalEndExample
, testCase "Add" $ roundtripTest i addExample
, testCase "Sub" $ roundtripTest i subExample
, testCase "Mul" $ roundtripTest i mulExample
, testCase "Div" $ roundtripTest i divExample
, testCase "Negate" $ roundtripTest i negateExample
-- , testCase "Choice value" $ roundtripTest i choiceValueExample
, testCase "Use" $ roundtripTest i useValueExample
, testCase "Cond" $ roundtripTest i condExample
-- ,testCase "Available money" $ roundtripTest i availableMoneyExample
, testResponse i "Invalid value"
(TestRoundtripSerialization (TypeId "Core.Value" (Proxy :: Proxy C.Value)) (JSON.String "invalid"))
(TestRoundtripSerialization
(TypeId "Core.Value" (Proxy @C.Value))
(JSON.String "invalid value")
)
assertSerializationError
]

observationTests :: InterpretJsonRequest -> TestTree
observationTests i = testGroup "Observation examples"
[ roundtripExampleTest i "True" trueExample
, roundtripExampleTest i "False" falseExample
, roundtripExampleTest i "And" andExample
, roundtripExampleTest i "Or" orExample
, roundtripExampleTest i "Not" notExample
-- , roundtripExampleTest i "Chose" choseExample
, roundtripExampleTest i "Value GE" valueGEExample
, roundtripExampleTest i "Value GT" valueGTExample
, roundtripExampleTest i "Value LT" valueLTExample
, roundtripExampleTest i "Value LE" valueLEExample
, roundtripExampleTest i "Value EQ" valueEQExample
[ testCase "True" $ roundtripTest i trueExample
, testCase "False" $ roundtripTest i falseExample
, testCase "And" $ roundtripTest i andExample
, testCase "Or" $ roundtripTest i orExample
, testCase "Not" $ roundtripTest i notExample
-- , testCase "Chose" $ roundtripTest i choseExample
, testCase "Value GE" $ roundtripTest i valueGEExample
, testCase "Value GT" $ roundtripTest i valueGTExample
, testCase "Value LT" $ roundtripTest i valueLTExample
, testCase "Value LE" $ roundtripTest i valueLEExample
, testCase "Value EQ" $ roundtripTest i valueEQExample
, testResponse i "Invalid observation"
(TestRoundtripSerialization (TypeId "Core.Observation" (Proxy :: Proxy C.Observation)) (JSON.String "invalid"))
assertSerializationError

]

-- TODO: Convert to property test once this task is done
-- SCP-4696 Improve thread usage
tokenTest :: InterpretJsonRequest -> TestTree
tokenTest i = testCase "Token test" $ do
-- Any token that is randomly generated by the interpreter should also pass the roundtrip test
token <- join $ generate $ runGenT $ arbitraryToken i
roundtripTest i token

partyTest :: InterpretJsonRequest -> TestTree
partyTest i = testCase "Party test" $ do
-- Any party that is randomly generated by the interpreter should also pass the roundtrip test
party <- join $ generate $ runGenT $ arbitraryParty i
roundtripTest i party


invalidType :: InterpretJsonRequest -> TestTree
invalidType i = testResponse i "Invalid type"
(TestRoundtripSerialization (TypeId "InvalidType" (Proxy :: Proxy ())) (JSON.String "invalid"))
assertUnknownType

roundtripExampleTest :: (HasTypeId a, ToJSON a) => InterpretJsonRequest -> TestName -> a -> TestTree
roundtripExampleTest i name example = exactMatch i name (serializationRequest example) (serializationSuccess example)
roundtripTest :: (HasTypeId a, ToJSON a) => InterpretJsonRequest -> a -> Assertion
roundtripTest interpret a = do
res <- interpret serializationRequest
successResponse @?= res
where
serializationRequest a = TestRoundtripSerialization (getTypeId a) $ toJSON a
serializationSuccess a = RequestResponse $ JSON.toJSON $ SerializationSuccess $ JSON.toJSON a
serializationRequest = TestRoundtripSerialization (getTypeId a) $ toJSON a
successResponse = RequestResponse $ toJSON $ SerializationSuccess $ toJSON a

assertSerializationError :: HasCallStack => Response JSON.Value -> Assertion
assertSerializationError = assertBool "The serialization response should be SerializationError" . isSerializationError
Expand Down
96 changes: 84 additions & 12 deletions marlowe-spec-test/src/Marlowe/Spec/LocalInterpret.hs
Original file line number Diff line number Diff line change
@@ -1,32 +1,104 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Marlowe.Spec.LocalInterpret where

import Arith (Int(..))
import qualified Data.Aeson as JSON
import Test.Tasty (TestName)
import Test.Tasty.Providers (TestTree)
import Marlowe.Spec.Core.Serialization.Json (localJsonRoundtripSerialization)
import Test.Tasty.HUnit (testCase, (@?=))
import Marlowe.Spec.Interpret (Response(..), Request(..))
import Semantics (playTrace, computeTransaction)
import Marlowe.Spec.TypeId (TypeId (..), fromTypeName)
import Marlowe.Spec.Core.Serialization.Json
import Data.Data (Proxy)
import Data.Aeson (Result (..),FromJSON,ToJSON)
import SemanticsTypes (Token(Token), Party (..))
import Test.QuickCheck (Gen, frequency, Arbitrary (arbitrary), generate)
import qualified Marlowe.Spec.Core.Arbitrary as RandomResponse


interpretLocal :: Request JSON.Value -> Response JSON.Value
interpretLocal :: Request JSON.Value -> IO (Response JSON.Value)
interpretLocal (TestRoundtripSerialization t v) =
RequestResponse
pure
$ RequestResponse
$ JSON.toJSON
$ localJsonRoundtripSerialization t v
interpretLocal (PlayTrace t c is) =
RequestResponse
pure
$ RequestResponse
$ JSON.toJSON
$ playTrace (Int_of_integer t) c is
interpretLocal (ComputeTransaction t s c) =
RequestResponse
pure
$ RequestResponse
$ JSON.toJSON
$ computeTransaction t s c
interpretLocal _ = RequestNotImplemented
interpretLocal (GenerateRandomValue t@(TypeId name _)) =
RequestResponse
<$> JSON.toJSON
<$> case name of
"Core.Token" -> RandomResponse.RandomValue <$> JSON.toJSON <$> generate arbitraryToken
"Core.Party" -> RandomResponse.RandomValue <$> JSON.toJSON <$> generate arbitraryParty
_ -> pure $ RandomResponse.UnknownType t

arbitraryToken :: Gen Token
arbitraryToken =
frequency
[(50, pure $ Token "" "")
,(50, Token <$> arbitrary <*> arbitrary)
]


-- | Some role names.
randomRoleNames :: [String]
randomRoleNames =
[
"Cy"
, "Noe"
, "Sten"
, "Cara"
, "Alene"
, "Hande"
, ""
, "I"
, "Zakkai"
, "Laurent"
, "Prosenjit"
, "Dafne Helge Mose"
, "Nonso Ernie Blanka"
, "Umukoro Alexander Columb"
, "Urbanus Roland Alison Ty Ryoichi"
, "Alcippe Alende Blanka Roland Dafne" -- NB: Too long for Cardano ledger.
]

-- | Part of the Fibonacci sequence.
fibonaccis :: Num a => [a]
fibonaccis = [2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584]


-- | Inverse-Fibanoncci frequencies.
fibonacciFrequencies :: Integral a => [a]
fibonacciFrequencies = (1000000 `div`) <$> fibonaccis


-- | Select an element of a list with propability proportional to inverse-Fibonacci weights.
arbitraryFibonacci :: [a] -> Gen a
arbitraryFibonacci = frequency . zip fibonacciFrequencies . fmap pure


arbitraryParty :: Gen Party
arbitraryParty = do
isAddress <- frequency [(2, pure True), (8, pure False)]
if isAddress
then Address <$> arbitrary
else Role <$> arbitraryFibonacci randomRoleNames

localJsonRoundtripSerialization :: TypeId -> JSON.Value -> SerializationResponse JSON.Value
localJsonRoundtripSerialization t@(TypeId name proxy) v = case fromTypeName name of
Nothing -> UnknownType t
(Just _) -> roundtrip proxy
where
roundtrip :: forall a. ToJSON a => FromJSON a => Proxy a -> SerializationResponse JSON.Value
roundtrip _ = case JSON.fromJSON v :: Result a of
Error str -> SerializationError str
Success c -> SerializationSuccess $ JSON.toJSON c

testLocal :: TestName -> Request JSON.Value -> Response JSON.Value -> TestTree
testLocal testName request expected = testCase testName
(interpretLocal request @?= expected)
2 changes: 1 addition & 1 deletion marlowe-spec-test/test/LocalSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ import Marlowe.Spec (tests)
import Marlowe.Spec.LocalInterpret (interpretLocal)

main :: IO ()
main = defaultMain $ tests (pure . interpretLocal)
main = defaultMain $ tests interpretLocal

0 comments on commit c8c67ad

Please sign in to comment.