packages feed

effectful-zoo-0.0.3.0: components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs

module Effectful.Zoo.Hedgehog.Api.Assert
  ( onNothingFail,
    onNothingFailM,
    onLeftFail,
    onLeftFailM,

    trapFail,
    trapFailJson,
    trapFailJsonPretty,
    trapFailYaml,

    failMessage,
    failWithCustom,

    byDurationM,
    byDeadlineM,
  ) where

import Data.Aeson
import Data.Aeson qualified as J
import Data.Aeson.Encode.Pretty qualified as J
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as LT
import Data.Text.Lazy.Encoding qualified as LT
import Data.Time.Clock (NominalDiffTime, UTCTime)
import Data.Time.Clock qualified as DTC
import Data.Yaml qualified as Y
import Effectful
import Effectful.Concurrent
import Effectful.Dispatch.Dynamic
import Effectful.Zoo.Core
import Effectful.Zoo.Error.Static
import Effectful.Zoo.Hedgehog.Api.Hedgehog
import Effectful.Zoo.Hedgehog.Api.Journal
import Effectful.Zoo.Hedgehog.Api.MonadAssertion
import Effectful.Zoo.Hedgehog.Effect.Hedgehog
import GHC.Stack qualified as GHC
import HaskellWorks.Prelude
import Hedgehog (MonadTest(..))
import Hedgehog qualified as H
import Hedgehog.Internal.Property qualified as H
import Hedgehog.Internal.Source qualified as H

onNothingFail :: forall a m. ()
  => HasCallStack
  => MonadTest m
  => Maybe a
  -> m a
onNothingFail mv =
  withFrozenCallStack $
    case mv of
      Just a -> pure a
      Nothing -> failWith Nothing "Expected Just, but got Nothing"

onNothingFailM :: forall a m. ()
  => HasCallStack
  => MonadTest m
  => m (Maybe a)
  -> m a
onNothingFailM f =
  withFrozenCallStack $
    f >>= onNothingFail

onLeftFail :: forall e a m. ()
  => HasCallStack
  => MonadTest m
  => Show e
  => Either e a
  -> m a
onLeftFail ea =
  withFrozenCallStack $
    case ea of
      Right a -> pure a
      Left e -> failWith Nothing $ "Expected Just, but got Left: " <> show e

onLeftFailM :: forall e a m. ()
  => HasCallStack
  => MonadTest m
  => Show e
  => m (Either e a)
  -> m a
onLeftFailM f =
  withFrozenCallStack $
    f >>= onLeftFail

failMessage :: ()
  => H.MonadTest m
  => CallStack
  -> String
  -> m a
failMessage cs =
  withFrozenCallStack $
    failWithCustom cs Nothing

failWithCustom :: ()
  => H.MonadTest m
  => CallStack
  -> Maybe H.Diff
  -> String
  -> m a
failWithCustom cs mdiff msg =
  H.liftTest $ H.mkTest (Left $ H.Failure (H.getCaller cs) msg mdiff, mempty)

trapFail :: forall e a r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: Hedgehog
  => Show e
  => Eff (Error e ': r) a
  -> Eff r a
trapFail f =
  withFrozenCallStack do
    r <- f & runError_
    case r of
      Right a -> pure a
      Left e  -> failWith Nothing $ show e

trapFailJson :: forall e a r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: Hedgehog
  => ToJSON e
  => Eff (Error e ': r) a
  -> Eff r a
trapFailJson f =
  withFrozenCallStack do
    r <- withFrozenCallStack $ f & runError_
    case r of
      Right a -> pure a
      Left e  -> do
        let msg = LT.unpack $ LT.decodeUtf8 $ J.encode e
        failWith Nothing msg

trapFailJsonPretty :: forall e a r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: Hedgehog
  => ToJSON e
  => Eff (Error e ': r) a
  -> Eff r a
trapFailJsonPretty f =
  withFrozenCallStack do
    r <- withFrozenCallStack $ f & runError_
    case r of
      Right a -> pure a
      Left e  -> do
        let msg = LT.unpack $ LT.decodeUtf8 $ J.encodePretty e
        failWith Nothing msg

trapFailYaml :: forall e a r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: Hedgehog
  => ToJSON e
  => Eff (Error e ': r) a
  -> Eff r a
trapFailYaml f =
  withFrozenCallStack do
    r <- withFrozenCallStack $ f & runError_
    case r of
      Right a -> pure a
      Left e  -> do
        let msg = T.unpack $ T.decodeUtf8 $ Y.encode e
        failWith Nothing msg

-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
--
-- Expiration of the deadline results in an assertion failure
byDeadlineM :: forall a r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: Hedgehog
  => r <: IOE
  => NominalDiffTime
  -> UTCTime
  -> String
  -> Eff r a
  -> Eff r a
byDeadlineM period deadline errorMessage f = GHC.withFrozenCallStack $ do
  start <- liftIO DTC.getCurrentTime
  a <- goM
  end <- liftIO DTC.getCurrentTime
  jot_ $ "Operation completed in " <> tshow (DTC.diffUTCTime end start)
  return a
  where goM = catchAssertion f $ \e -> do
          currentTime <- liftIO DTC.getCurrentTime
          if currentTime < deadline
            then do
              threadDelay (floor (DTC.nominalDiffTimeToSeconds period * 1000000))
              goM
            else do
              jotShow_ currentTime
              void $ failMessage GHC.callStack $ "Condition not met by deadline: " <> errorMessage
              throwAssertion e

-- | Run the operation 'f' once a second until it returns 'True' or the duration expires.
--
-- Expiration of the duration results in an assertion failure
byDurationM :: forall b r. ()
  => HasCallStack
  => r <: Concurrent
  => r <: Error H.Failure
  => r <: IOE
  => r <: Hedgehog
  => NominalDiffTime
  -> NominalDiffTime
  -> String
  -> Eff r b
  -> Eff r b
byDurationM period duration errorMessage f = GHC.withFrozenCallStack $ do
  deadline <- DTC.addUTCTime duration <$> liftIO DTC.getCurrentTime
  byDeadlineM period deadline errorMessage f