packages feed

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

{-# LANGUAGE RankNTypes #-}
module Test.Observation
  ( prop_observation
  ) where

import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef')
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range

import Hedgehog.Lockstep

-- ---------------------------------------------------------------------------
-- Exercises 'Observation' beyond plain equality.
--
-- The system under test returns @(Int, String)@ where the @String@ is a
-- formatted, human-readable version of the @Int@ ("count=42"). The
-- model ignores formatting concerns and returns @(Int, Int)@. The
-- observation projects both sides to a common @Int@ shape via
-- 'ObservePair' + 'ObserveProject' + 'ObserveEq'.
-- ---------------------------------------------------------------------------

type Counter = IORef Int

type Model = Int

newCounter :: IO Counter
newCounter = newIORef 0

resetCounter :: Counter -> IO ()
resetCounter ref = writeIORef ref 0

bump :: Counter -> Int -> IO (Int, String)
bump ref n = do
  modifyIORef' ref (+ n)
  v <- readIORef ref
  pure (v, "count=" <> show v)

-- Inputs

data BumpInput v = BumpInput !Int
  deriving stock (Show)

instance FunctorB BumpInput where bmap _ (BumpInput n) = BumpInput n
instance TraversableB BumpInput where btraverse _ (BumpInput n) = pure (BumpInput n)

-- The model returns @(Int, Int)@ (the second Int is the same as the
-- first, deliberately matching the textual count). The real output is
-- @(Int, String)@. ObservePair lets us compare the @Int@ slot directly
-- and project the formatted @String@ slot back to @Int@ before comparing.
cmdBump :: Counter -> LockstepCmd (PropertyT IO) Model
cmdBump ref = LockstepCmd
  { lsCmdGen = \_ -> Just $ BumpInput <$> Gen.int (Range.linear 1 5)

  , lsCmdExec = \(BumpInput n) -> evalIO (bump ref n)

  , lsCmdModel = \st (BumpInput n) ->
      let v = getModel st + n
      in ((v, v), v)

  , lsCmdRequire = \_ _ -> True

  , lsCmdObserve =
      runObservation $
        ObservePair
          ObserveEq
          (ObserveProject id parseCount)

  , lsCmdInvariants = \_ _ -> pure ()

  , lsCmdTag = \_ _ _ -> []
  }
  where
    -- Strip "count=" prefix from the formatted string.
    parseCount :: String -> Int
    parseCount s =
      case drop (length ("count=" :: String)) s of
        digits -> read digits

prop_observation :: Property
prop_observation =
  lockstepPropertyWith
    0
    20
    newCounter
    resetCounter
    (\ref -> [cmdBump ref])