packages feed

method-0.1.0.0: src/Test/Method/Monitor.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Module : Test.Method.Monitor
-- Description:
-- License: BSD-3
-- Maintainer: autotaker@gmail.com
-- Stability: experimental
--
-- Validating method calls by monitoring
module Test.Method.Monitor
  ( Event,
    Monitor,
    newMonitor,
    watch,
    watchBy,
    listenEventLog,
    withMonitor,
    withMonitor_,
    times,
    call,
  )
where

import Control.Method (Method (Args, Base, Ret), decorate)
import Data.Coerce (coerce)
import RIO
  ( MonadIO (liftIO),
    MonadUnliftIO,
    readSomeRef,
  )
import Test.Method.Matcher (Matcher)
import Test.Method.Monitor.Internal
  ( EqUptoShow (EqUptoShow),
    Event (Enter, Leave),
    Monitor (monitorTrace),
    logEvent,
    newMonitor,
    tick,
  )

-- | @watchBy fArgs fRet monitor method@ decorates @method@
-- so that @monitor@ logs the method calls.
-- This function is suited for monitoring multiple methods.
--
-- @fArgs@ and @fRet@ is converter for arguments/return values of given method.
--
-- @
-- foo :: Int -> IO String
-- foo = ...
-- bar :: Int -> String -> IO ()
-- bar = ...
--
-- data MonitorArgs = FooArgs Int | BarArgs (Int,String) deriving(Eq,Show)
-- data MonitorRet = FooRet String | BarRet () deriving(Eq, Show)
--
-- foo' :: Monitor MonitorArgs MonitorRet -> Int -> IO String
-- foo' monitor = watch monitor (FooArgs . toTuple) FooRet foo
-- bar' :: Monitor MonitorArgs MonitorRet -> Int -> String -> IO ()
-- bar' monitor = watch monitor (BarArgs . toTuple) BarRet bar
-- @
{-# INLINEABLE watchBy #-}
watchBy ::
  (Method method, MonadUnliftIO (Base method)) =>
  (Args method -> args) ->
  (Ret method -> ret) ->
  Monitor args ret ->
  method ->
  method
watchBy fargs fret m method = method'
  where
    method' = decorate before after (const method)
    before args = do
      t <- tick m
      logEvent m (Enter t (fargs args))
      pure t
    after t result = do
      t' <- tick m
      logEvent m (Leave t' t $ coerce $ fmap fret result)

-- | Simplified version of 'watchBy'. It is suitable to monitor single method.
{-# INLINE watch #-}
watch ::
  (Method method, MonadUnliftIO (Base method)) =>
  Monitor (Args method) (Ret method) ->
  method ->
  method
watch = watchBy id id

-- | Get current event logs from monitor
{-# INLINE listenEventLog #-}
listenEventLog :: MonadIO m => Monitor args ret -> m [Event args ret]
listenEventLog m = reverse <$> readSomeRef (monitorTrace m)

-- | @'times' countMatcher eventMatcher@ counts events that matches @eventMatcher@,
--   and then the count matches @countMatcher@
times :: Matcher Int -> Matcher (Event args ret) -> Matcher [Event args ret]
times countMatcher eventMatcher =
  countMatcher . length . filter eventMatcher

-- | @'call' matcher@ matches method call whose arguments matches @matcher@
call :: Matcher args -> Matcher (Event args ret)
call argsM (Enter _ args) = argsM args
call _ Leave {} = False

-- | @withMonitor f@ calls @f@ with 'Monitor',
-- and then returns monitored event logs during the function call
-- in addition to the return value of the function call
{-# INLINE withMonitor #-}
withMonitor :: MonadIO m => (Monitor args ret -> m a) -> m (a, [Event args ret])
withMonitor f = do
  monitor <- liftIO newMonitor
  r <- f monitor
  logs <- listenEventLog monitor
  pure (r, logs)

-- | @withMonitor_ f@ calls @f@ with 'Monitor', and returns event logs during the call.
{-# INLINE withMonitor_ #-}
withMonitor_ :: MonadIO m => (Monitor args ret -> m ()) -> m [Event args ret]
withMonitor_ f = do
  monitor <- liftIO newMonitor
  f monitor
  listenEventLog monitor