packages feed

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

{-# LANGUAGE TypeApplications #-}
module Test.UnitCoverage
  ( prop_applyOp
  , prop_gvarLabel
  , prop_mapGVar
  , prop_mkGVarId
  ) where

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

import Hedgehog.Lockstep

-- ---------------------------------------------------------------------------
-- applyOp: direct unit tests for each Op constructor
-- ---------------------------------------------------------------------------

prop_applyOp :: Property
prop_applyOp = withTests 1 $ property $ do
  applyOp (OpId :: Op Int Int) 7   === Just 7
  applyOp OpFst (1 :: Int, 'a')    === Just 1
  applyOp OpSnd (1 :: Int, 'a')    === Just 'a'
  applyOp OpLeft  (Left  'x' :: Either Char Int) === Just 'x'
  applyOp OpLeft  (Right 9   :: Either Char Int) === Nothing
  applyOp OpRight (Left  'x' :: Either Char Int) === Nothing
  applyOp OpRight (Right 9   :: Either Char Int) === Just 9
  applyOp (OpRight >>> OpFst)
          (Right (1 :: Int, 'a') :: Either String (Int, Char))
    === Just 1

-- ---------------------------------------------------------------------------
-- gvarLabel: render projection chains
-- ---------------------------------------------------------------------------

prop_gvarLabel :: Property
prop_gvarLabel = withTests 1 $ property $ do
  -- Use a Concrete-phase Var so we can construct it directly. The label
  -- only depends on the projection, not on the underlying variable.
  let v :: Var (Either String (Int, Int)) Concrete
      v = Var (Concrete (Right (1, 2)))
  gvarLabel (mkGVarId v :: GVar (Either String (Int, Int)) Concrete) === "id"
  gvarLabel (mkGVar v (OpRight :: Op (Either String (Int, Int)) (Int, Int)))
    === "right"
  gvarLabel (mkGVar v ((OpRight >>> OpFst) :: Op (Either String (Int, Int)) Int))
    === "right.fst"

-- ---------------------------------------------------------------------------
-- mapGVar: composing an additional projection onto an existing GVar
--
-- Build a GVar that projects the @Right@ branch of an @Either@, then use
-- 'mapGVar' to extend it with @OpFst@ to land on the first tuple element.
-- The resulting GVar resolves to that element via 'concreteGVar' and
-- carries the composed projection in its label.
-- ---------------------------------------------------------------------------

prop_mapGVar :: Property
prop_mapGVar = withTests 1 $ property $ do
  let v :: Var (Either String (Int, Int)) Concrete
      v = Var (Concrete (Right (7, 9)))
      gvRight :: GVar (Int, Int) Concrete
      gvRight = mkGVar v (OpRight :: Op (Either String (Int, Int)) (Int, Int))
      gvFst :: GVar Int Concrete
      gvFst = mapGVar OpFst gvRight
  gvarLabel gvFst    === "right.fst"
  concreteGVar gvFst === Just 7
  -- Mapping with OpId is the identity (modulo the label suffix).
  concreteGVar (mapGVar OpId gvRight) === Just (7, 9)

-- ---------------------------------------------------------------------------
-- mkGVarId: an integration test using identity projection
--
-- The action returns a single Int. A later command takes a GVar Int that
-- references the prior result via mkGVarId (no projection). The Echo
-- command simply returns the GVar's concrete value, demonstrating that
-- mkGVarId resolves correctly across both phases.
-- ---------------------------------------------------------------------------

type Model = ()

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

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

data EchoInput v = EchoInput !(GVar Int v)
  deriving stock (Show)

instance FunctorB EchoInput where
  bmap f (EchoInput gv) = EchoInput (bmap f gv)
instance TraversableB EchoInput where
  btraverse f (EchoInput gv) = EchoInput <$> btraverse f gv

cmdPut :: IORef Int -> LockstepCmd (PropertyT IO) Model
cmdPut ref = LockstepCmd
  { lsCmdGen        = \_ -> Just $ PutInput <$> Gen.int (Range.linear 0 100)
  , lsCmdExec       = \(PutInput n) -> evalIO (writeIORef ref n) >> pure n
  , lsCmdModel      = \_ (PutInput n) -> (n, ())
  , lsCmdRequire    = \_ _ -> True
  , lsCmdObserve    = \expected actual -> expected === actual
  , lsCmdInvariants = \_ _ -> pure ()
  , lsCmdTag        = \_ _ _ -> []
  }

cmdEcho :: LockstepCmd (PropertyT IO) Model
cmdEcho = LockstepCmd
  { lsCmdGen = \st ->
      case varsOfType @Int st of
        []   -> Nothing
        vars -> Just $ do
          var <- Gen.element vars
          pure $ EchoInput (mkGVarId var)

  , lsCmdExec = \(EchoInput gv) ->
      pure (concreteGVar gv)

  , lsCmdModel = \st (EchoInput gv) ->
      (resolveGVar gv (getEntries st), ())

  , lsCmdRequire = \_ _ -> True

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

  , lsCmdInvariants = \_ _ -> pure ()
  , lsCmdTag        = \_ _ _ -> []
  }

prop_mkGVarId :: Property
prop_mkGVarId =
  lockstepPropertyWith
    ()
    20
    (newIORef 0)
    (\ref -> writeIORef ref 0)
    (\ref -> [cmdPut ref, cmdEcho])