packages feed

mockcat-1.4.0.0: src/Test/MockCat/Internal/MockRegistry.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
module Test.MockCat.Internal.MockRegistry
  ( attachVerifierToFn
  , lookupVerifierForFn
  , register
  , registerUnitMeta
  , lookupUnitMeta
  , UnitMeta
  , withUnitGuard
  , withAllUnitGuards
  , isGuardActive
  , getLastRecorder
  , resetMockHistory
  ) where

import Test.MockCat.Internal.Registry.Core
  ( attachVerifierToFn
  , lookupVerifierForFn
  , registerUnitMeta
  , lookupUnitMeta
  , UnitMeta
  , withUnitGuard
  , withAllUnitGuards
  , isGuardActive
  , getLastRecorder
  , resetMockHistory
  )
import GHC.IO (evaluate)
import Control.Concurrent.STM (TVar, atomically, writeTVar)
import Test.MockCat.Internal.Types (MockName, InvocationRecorder(..), InvocationRecord, perform)
import Data.Proxy (Proxy(..))
import Data.Dynamic
import Test.MockCat.Internal.Builder (invocationRecord, appendCalledParams)
import Type.Reflection (TyCon, splitApps, typeRep, typeRepTyCon)
import Data.Typeable (eqT)
import Data.Type.Equality ((:~:) (Refl))

ioTyCon :: TyCon
ioTyCon = typeRepTyCon (typeRep @(IO ()))

isIOType :: forall a. Typeable a => Proxy a -> Bool
isIOType _ =
  case splitApps (typeRep @a) of
    (tc, _) -> tc == ioTyCon

-- | Wrap a function value for unit-typed stubs so that calls are tracked.
-- This uses the UnitMeta guard to avoid double-counting when both the tracked
-- and base values are registered. The wrapper will append an invocation to the
-- recorder's TVar when appropriate.
wrapUnitStub ::
  forall fn.
  Typeable fn =>
  TVar (InvocationRecord ()) ->
  UnitMeta ->
  fn ->
  fn
wrapUnitStub ref meta value =
  let trackedValue = perform $ do
        guardActive <- isGuardActive meta
        if guardActive || isIOType (Proxy :: Proxy fn)
          then pure value
          else do
            appendCalledParams ref ()
            pure value
  in
    trackedValue


-- | Register a recorder for a function in the global mock registry.
-- This handles the special '()' (unit) case by creating a tracked wrapper
-- and registering both the tracked and base values so StableName lookup
-- succeeds regardless of which closure is later passed for verification.
register ::
  forall fn params.
  ( Typeable params
  , Typeable (InvocationRecorder params)
  , Typeable fn
  ) =>
  Maybe MockName ->
  InvocationRecorder params ->
  fn ->
  IO fn
register name recorder@(InvocationRecorder {invocationRef = ref}) fn = do
  baseValue <- evaluate fn
  case eqT :: Maybe (params :~: ()) of
    Just Refl -> do
      meta <- registerUnitMeta ref
      atomically $ writeTVar ref invocationRecord
      let trackedValue = wrapUnitStub ref meta baseValue
      withUnitGuard meta $ do
        attachVerifierToFn trackedValue (name, recorder)
        attachVerifierToFn baseValue (name, recorder)
      pure trackedValue
    Nothing -> do
      attachVerifierToFn baseValue (name, recorder)
      pure baseValue