packages feed

hedgehog-lockstep-0.1.0.0: test/Test/ReaderKV.hs

{-# LANGUAGE RankNTypes #-}
module Test.ReaderKV
  ( prop_readerKV
  ) where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Data.IORef (IORef, newIORef, readIORef, modifyIORef', writeIORef)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range

import Hedgehog.Lockstep

-- ---------------------------------------------------------------------------
-- Exercises 'lockstepPropertyWithM': commands written in
-- @ReaderT Store (PropertyT IO)@ rather than @PropertyT IO@ directly.
-- The runner ('runReaderT') is supplied separately, so command bodies
-- read the store from the environment instead of closing over it.
-- ---------------------------------------------------------------------------

type Store = IORef (Map String Int)

type AppM = ReaderT Store (PropertyT IO)

type Model = Map String Int

newStore :: IO Store
newStore = newIORef Map.empty

resetStore :: Store -> IO ()
resetStore ref = writeIORef ref Map.empty

-- Inputs

data PutInput v = PutInput !String !Int
  deriving stock (Show)

instance FunctorB PutInput where bmap _ (PutInput k v) = PutInput k v
instance TraversableB PutInput where btraverse _ (PutInput k v) = pure (PutInput k v)

data GetInput v = GetInput !String
  deriving stock (Show)

instance FunctorB GetInput where bmap _ (GetInput k) = GetInput k
instance TraversableB GetInput where btraverse _ (GetInput k) = pure (GetInput k)

genKey :: Gen String
genKey = Gen.element ["a", "b", "c"]

genVal :: Gen Int
genVal = Gen.int (Range.linear 0 100)

-- Commands run in 'AppM'. Note: the exec functions never see a 'Store'
-- argument; they read it from the ReaderT environment. The model
-- functions are unchanged, since the model is pure.
cmdPut :: LockstepCmd AppM Model
cmdPut = LockstepCmd
  { lsCmdGen = \_ -> Just $ PutInput <$> genKey <*> genVal

  , lsCmdExec = \(PutInput k v) -> do
      ref <- ask
      liftIO $ modifyIORef' ref (Map.insert k v)

  , lsCmdModel = \st (PutInput k v) ->
      ((), Map.insert k v (getModel st))

  , lsCmdRequire = \_ _ -> True

  , lsCmdObserve = \() () -> pure ()

  , lsCmdInvariants = \_ _ -> pure ()

  , lsCmdTag = \_ _ _ -> []
  }

cmdGet :: LockstepCmd AppM Model
cmdGet = LockstepCmd
  { lsCmdGen = \_ -> Just $ GetInput <$> genKey

  , lsCmdExec = \(GetInput k) -> do
      ref <- ask
      liftIO $ Map.lookup k <$> readIORef ref

  , lsCmdModel = \st (GetInput k) ->
      let model = getModel st
      in (Map.lookup k model, model)

  , lsCmdRequire = \_ _ -> True

  , lsCmdObserve = \expected actual -> expected === actual

  , lsCmdInvariants = \_ _ -> pure ()

  , lsCmdTag = \_ _ _ -> []
  }

prop_readerKV :: Property
prop_readerKV =
  lockstepPropertyWithM
    Map.empty
    50
    newStore
    resetStore
    (\store m -> runReaderT m store)
    (\_store -> [cmdPut, cmdGet])