packages feed

morley-0.3.0: test/Test/Interpreter/A1/Feather.hs

{-# LANGUAGE DeriveAnyClass, DerivingStrategies #-}

-- | Tests for feathering as described in TZIP-A1.

module Test.Interpreter.A1.Feather
  ( featherSpec
  ) where

import Control.Lens (makeLenses, (%=), (+=), (-=), (.=))
import Control.Monad.Except (Except, runExcept, throwError)
import Data.Singletons (SingI)
import Test.Hspec (Spec)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck (Gen, arbitrary, forAll, frequency, listOf, suchThat)
import Test.QuickCheck.Arbitrary.Generic (genericArbitrary)
import Test.QuickCheck.Instances.Natural ()

import Lorentz (View, Void_)
import qualified Lorentz as L
import Lorentz.Test
import Michelson.Interpret (MichelsonFailed(..))
import Michelson.Interpret.Pack (packValue')
import Michelson.Test
import qualified Michelson.Typed as T
import qualified Michelson.Untyped as U
import Tezos.Address
import Tezos.Core
import Tezos.Crypto
import Util.Named

data Outcome = Outcome
  { _oCounter :: !Natural
  , _oList :: ![Natural]
  , _oSum :: !Natural
  }

makeLenses ''Outcome

featherSpec :: Spec
featherSpec =
  specWithContractL "contracts/A1/counter.mtz" $ \counter ->
  specWithContractL "contracts/A1/feather.mtz" $ \feather ->
  specWithContractL "contracts/A1/caller_add.mtz" $ \callerAdd ->
  specWithContractL "contracts/A1/caller_append.mtz" $ \callerAppend ->
  specImpl counter feather callerAdd callerAppend

data CounterParameter
  = AntibumpCounter
  | BumpCounter
  | ResetCounter Natural
  | GetCount (View () Natural)
  | HashCount (Void_ () ByteString)
  deriving stock Generic
  deriving anyclass T.IsoValue
type CounterStorage = Natural

type FeatherParameter = ((), Maybe Natural)
type FeatherStorage = (Address, Maybe Address)

type CallerParameter = Either Natural ()
type CallerAddStorage = (Natural, Address)
type CallerAppendStorage = ([Natural], Address)

-- Actions that can be performed by this scenario.
data Action
  = Antibump
  -- ^ Antibump counter
  | Bump
  -- ^ Bump counter
  | Reset !Natural
  -- ^ Reset counter
  | HashCounter
  -- ^ Get counter's hash using void entry point (which implies that
  -- everything else should fail)
  | Add !Natural
  -- ^ Add a constant to caller-add
  | AddRecord
  -- ^ Add counter value to caller-add
  | Append !Natural
  -- ^ Append a constant to caller-append
  | AppendRecord
  -- ^ Append counter value to caller-append
  deriving (Show, Generic)

genAction :: Gen Action
genAction = genericArbitrary

data Fixture = Fixture
  { fActions :: ![Action]
  , fInitialCounter :: !Natural
  , fInitialList :: ![Natural]
  , fInitialSum :: !Natural
  } deriving (Show)

-- We generate 'HashCounter' with smaller probablity, because otherwise
-- tests will often stop quickly (they stop as soon as 'HashCounter'
-- occurs).
genFixture :: Gen Fixture
genFixture = do
  fActions <-
    listOf $ frequency
      [ (26, genAction `suchThat` notHashCounter)
      , (1, pure HashCounter)
      ]
  fInitialCounter <- arbitrary
  fInitialList <- arbitrary
  fInitialSum <- arbitrary
  pure Fixture {..}
  where
    notHashCounter =
      \case
        HashCounter -> False
        _ -> True

data ExpectedFail
  = NegativeCounter
  | HashCounterCalled !ByteString

expectedOutcome :: Fixture -> Either ExpectedFail Outcome
expectedOutcome Fixture {..} =
  runExcept $ execStateT (mapM_ step fActions) start
  where
    start = Outcome
      { _oCounter = fInitialCounter
      , _oList = fInitialList
      , _oSum = fInitialSum
      }
    step :: Action -> StateT Outcome (Except ExpectedFail) ()
    step = \case
      Antibump -> use oCounter >>= \case
        0 -> throwError NegativeCounter
        _ -> oCounter -= 1
      Bump -> oCounter += 1
      Reset x -> oCounter .= x
      HashCounter ->
        throwError .
        HashCounterCalled .
        sha512 .
        packValue' .
        T.toVal =<<
        use oCounter
      Add x -> oSum += x
      AddRecord -> (oSum +=) =<< use oCounter
      Append x -> oList %= (x:)
      AppendRecord -> do
        c <- use oCounter
        oList %= (c:)

specImpl ::
     (U.Contract, L.Contract CounterParameter CounterStorage)
  -> (U.Contract, L.Contract FeatherParameter FeatherStorage)
  -> (U.Contract, L.Contract CallerParameter CallerAddStorage)
  -> (U.Contract, L.Contract CallerParameter CallerAppendStorage)
  -> Spec
specImpl (_, counter) (_, feather) (_, callerAdd) (_, callerAppend) =
  prop "A mix of random actions is handled as expected" $
    forAll genFixture $ \fixture ->
      integrationalTestProperty (scenario fixture)
  where
    scenario :: Fixture -> IntegrationalScenario
    scenario fixture@Fixture {..} = do
      let
        initBalance = unsafeMkMutez 100

      counterContractAddress@(T.ContractAddr counterAddress) <-
        lOriginate counter "counter" fInitialCounter initBalance

      let
        defaultFeatherValue :: FeatherStorage
        defaultFeatherValue =
          (counterAddress, Nothing @Address)

      featherContractAddress@(T.ContractAddr featherAddress) <-
        lOriginate feather "feather" defaultFeatherValue initBalance
      cAddContractAddress <-
        lOriginate callerAdd "caller-add"
        (fInitialSum, featherAddress) initBalance
      cAppendContractAddress <-
        lOriginate callerAppend "caller-append"
        (fInitialList, featherAddress) initBalance

      let
        transfer' ::
          forall t . (T.IsoValue t, SingI (T.ToT t), T.HasNoOp (T.ToT t))
          => T.ContractAddr t -> t -> IntegrationalScenarioM ()
        transfer' addr param =
          lTransfer (#from .! genesisAddress) (#to .! addr)
                    (unsafeMkMutez 0) param

        transferToCounter :: CounterParameter -> IntegrationalScenarioM ()
        transferToCounter = transfer' counterContractAddress

        transferToAdd :: CallerParameter -> IntegrationalScenarioM ()
        transferToAdd = transfer' cAddContractAddress

        transferToAppend :: CallerParameter -> IntegrationalScenarioM ()
        transferToAppend = transfer' cAppendContractAddress

        performAction :: Action -> IntegrationalScenarioM ()
        performAction = \case
          Antibump -> transferToCounter AntibumpCounter
          Bump -> transferToCounter BumpCounter
          Reset x -> transferToCounter $ ResetCounter x
          HashCounter -> transferToCounter $ HashCount (L.mkVoid ())
          Add x -> transferToAdd $ Left x
          AddRecord -> transferToAdd $ Right ()
          Append x -> transferToAppend $ Left x
          AppendRecord -> transferToAppend $ Right ()

      mapM_ performAction fActions

      let
        checkHash :: ByteString -> MichelsonFailed -> Bool
        checkHash expectedHash = (== MichelsonFailedWith (T.toVal expectedHash))
        validator :: IntegrationalValidator
        validator = case expectedOutcome fixture of
          Left NegativeCounter ->
            Left $ expectMichelsonFailed (const True) counterAddress
          Left (HashCounterCalled expectedHash) ->
            Left $ expectMichelsonFailed (checkHash expectedHash) counterAddress
          Right (Outcome {..}) -> Right $ composeValidatorsList
            [ -- Feather must always be called twice and the second
              -- call must set its storage to its initial value.
              lExpectStorageConst featherContractAddress defaultFeatherValue
            , -- Counter, caller-add and caller-append have values
              -- according to 'Outcome'.
              lExpectStorageConst counterContractAddress _oCounter
            , lExpectStorageConst cAddContractAddress (_oSum, featherAddress)
            , lExpectStorageConst cAppendContractAddress (_oList, featherAddress)
            , -- Balances must not change
              expectBalance counterAddress initBalance
            , expectBalance featherAddress initBalance
            , lExpectBalance cAddContractAddress initBalance
            , lExpectBalance cAppendContractAddress initBalance
            ]

      validate validator