packages feed

error-context-0.2.0.0: test/Control/Error/Context/Test.hs

{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE QuasiQuotes         #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}

module Control.Error.Context.Test (tests) where

import Data.Aeson
import           Control.Error.Context
import           Control.Exception           (Exception (..), throw, throwIO)
import           Control.Monad
import           Control.Monad.Catch         (MonadCatch, catch, throwM, try)
import           Control.Monad.IO.Class
import qualified Data.HashMap.Strict as HashMap
import Data.HashMap.Strict (HashMap)
import           Data.Text                   (Text)
import           Katip
import           Test.Tasty
import           Test.Tasty.HUnit

tests :: TestTree
tests = testGroup "Tests" $
  [ testGroup "Simple (ErrorContextT)" (testsWithConf testConfSimple)
  , testGroup "Katip (ErrorContextKatipT)" (testsWithConf testConfKatip)
  , testGroup "Test Examples" testExamples
  ]

testExamples :: [TestTree]
testExamples = 
  [ testCase "simpleExample" testExample ]

testsWithConf :: TestConf -> [TestTree]
testsWithConf conf =
    [ testCase "Contextualize IO Exception"
        (testContextualizeIOException conf)
    , testCase "throwM"
        (testThrow conf)
    , testCase "catchAnyWithContext"
        (testCatchAnyWithContext conf)
    , testCase "catchAnyWithContext/pure"
        (testCatchAnyWithContextPure conf)
    , testCase "catchAnyWithoutContext"
        (testCatchAnyWithoutContext conf)
    , testCase "catchAnyWithoutContext/pure"
        (testCatchAnyWithoutContextPure conf)
    , testCase "Catch context-enriched exception without context"
        (testCatchWithoutContext conf)
    , testCase "Contextualize error value"
        (testContextualizeErrorValue conf)
    , testCase "Forgetting error context"
        (testForgetErrorContext conf)
    , testCase "Dumping error context"
        (testDumpErrorContext conf)
    , testCase "Throw and catch"
        (testThrowAndCatch conf)
    , testCase "Catch non-contextualized exception with context"
        (testNonContextualizedCatchWithContext conf)
    , testCase "ensureExceptionContext"
        (testEnsureExceptionContext conf)
    , testCase "catch head exception"
        (testCatchHeadException conf)
    , testCase "tryAnyWithoutContext"
        (testTryAnyWithoutContext conf)
    , testCase "tryAnyWithoutContext/pure"
        (testTryAnyWithoutContextPure conf)
    , testCase "tryAnyWithContext"
        (testTryAnyWithContext conf)
    , testCase "tryAnyWithContext/pure"
        (testTryAnyWithContextPure conf)
    , testCase "tryWithContext"
        (testTryWithContext conf)
    , testCase "tryWithContext/pure"
        (testTryWithContextPure conf)
    , testCase "tryWithoutContext"
        (testTryWithoutContext conf)
    , testCase "tryWithoutContext/pure"
        (testTryWithoutContextPure conf)
    , testCase "Throw and catch"
        (testThrowAndCatch conf)
    , testCase "contextKvRetrieval"
        (testContextKv conf)
    , testCase "contextKvOverwrite"
        (testContextKvOverwrite conf)
    ]

data TestException = TestException deriving (Show, Eq)

instance Exception TestException

data TestConf where
  TestConf :: forall m. (MonadIO m, MonadCatch m, MonadErrorContext m) =>
              { runStackT         :: forall a. m a -> IO a }
           -> TestConf

testConfKatip :: TestConf
testConfKatip =
  TestConf { runStackT = \ m -> do
               logEnv <- liftIO $ initLogEnv "test-suite" "test"
               runKatipContextT logEnv () (Namespace []) $ runErrorContextKatipT m
           }

testConfSimple :: TestConf
testConfSimple =
  TestConf { runStackT = runErrorContextT }

extractKVs :: ErrorContext -> HashMap Text Value
extractKVs (ErrorContext kvs _namespace) =
  kvs

testContextualizeIOException :: TestConf -> Assertion
testContextualizeIOException TestConf { .. } = do
  Left (ErrorWithContext ctx TestException) <- try . runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $
    liftIO failingIOException
  ["A", "B"] @=? errorContextNamespace ctx

  where failingIOException :: IO ()
        failingIOException =
          throwIO TestException

testThrow :: TestConf -> Assertion
testThrow TestConf { .. } = do
  catch (runStackT (throwM TestException)) $ \ someExn -> do
    let Just (ErrorWithContext _ctx someInnerExn) = fromException someExn
    liftIO $ Just TestException @=? fromException someInnerExn

testCatchWithoutContext :: TestConf -> Assertion
testCatchWithoutContext TestConf { .. } = do
  TestException <- runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $
    catchWithoutContext (throwM TestException) $ \ (exn :: TestException) -> do
      pure exn
  pure ()

testContextualizeErrorValue :: TestConf -> Assertion
testContextualizeErrorValue TestConf { .. } = do
  ErrorWithContext ctx TestException <- runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $
    errorContextualize TestException
  ["A", "B"] @=? errorContextNamespace ctx

testForgetErrorContext :: TestConf -> Assertion
testForgetErrorContext TestConf { .. } = do
  errWithCtx @ (ErrorWithContext _ctx TestException) <- runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $
    errorContextualize TestException
  TestException @=? errorContextForget errWithCtx

testDumpErrorContext :: TestConf -> Assertion
testDumpErrorContext TestConf { .. } = do
  errWithCtx @ (ErrorWithContext _ctx _exn) <- runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $
    errorContextualize TestException
  errorWithContextDump errWithCtx

testThrowAndCatch :: TestConf -> Assertion
testThrowAndCatch TestConf { .. } = do
  void . runStackT $
    catch (throwM TestException) $ \ TestException -> pure ()

testCatchAnyWithContext :: TestConf -> Assertion
testCatchAnyWithContext TestConf { .. } = do
  catchAnyWithContext (runStackT (throwM TestException)) $
    \ (ErrorWithContext _ctx someExn) -> do
      Just TestException @=? fromException someExn

testCatchAnyWithContextPure :: TestConf -> Assertion
testCatchAnyWithContextPure TestConf { .. } = do
  catchAnyWithContext (runStackT (throw TestException)) $
    \ (ErrorWithContext _ctx someExn) -> do
      Just TestException @=? fromException someExn

testCatchAnyWithoutContext :: TestConf -> Assertion
testCatchAnyWithoutContext TestConf { .. } = do
  catchAnyWithoutContext (runStackT (throwM TestException)) $
    \ someExn -> do
      Just TestException @=? fromException someExn

testCatchAnyWithoutContextPure :: TestConf -> Assertion
testCatchAnyWithoutContextPure TestConf { .. } = do
  catchAnyWithoutContext (runStackT (throw TestException)) $
    \ someExn -> do
      Just TestException @=? fromException someExn

testNonContextualizedCatchWithContext :: TestConf -> Assertion
testNonContextualizedCatchWithContext TestConf { .. } = do
  ErrorWithContext ctx TestException <- runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $ do
    catchWithContext throwPureException $ \ (exn :: ErrorWithContext TestException) -> do
      pure exn
  [] @=? errorContextNamespace ctx

  where throwPureException = throw TestException

testEnsureExceptionContext :: TestConf -> Assertion
testEnsureExceptionContext TestConf { .. } = do
  Left someExn <- try . runStackT $
    withErrorNamespace "A" $
    withErrorNamespace "B" $ do
    ensureExceptionContext $ do
      throw TestException
  let Just (ErrorWithContext ctx someExnWithoutCtx) = fromException someExn
  Just TestException @=? fromException someExnWithoutCtx
  ["A", "B"] @=? errorContextNamespace ctx

testCatchHeadException :: TestConf -> Assertion
testCatchHeadException TestConf { .. } = do
  Left errWithCtx <- tryAnyWithContext . runStackT $ do
    withErrorNamespace "Here I am, calling head on an empty list!" $
      ensureExceptionContext $ seq (head []) (pure ())
  let (ErrorWithContext _ctx _exnWithoutCtx) = errWithCtx
  putStrLn . displayException $ errWithCtx

testTryAnyWithContext :: TestConf -> Assertion
testTryAnyWithContext TestConf { .. } = do
  Left (ErrorWithContext _ctx someExn) <- tryAnyWithContext . runStackT $ do
    void $ throwM TestException
    pure ()
  Just TestException @=? fromException someExn

testTryAnyWithContextPure :: TestConf -> Assertion
testTryAnyWithContextPure TestConf { .. } = do
  Left (ErrorWithContext _ctx someExn) <- tryAnyWithContext . runStackT $
    seq (throw TestException) (pure ())
  Just TestException @=? fromException someExn

testTryAnyWithoutContext :: TestConf -> Assertion
testTryAnyWithoutContext TestConf { .. } = do
  Left someExn <- tryAnyWithoutContext . runStackT $ do
    void $ throwM TestException
    pure ()
  Just TestException @=? fromException someExn

testTryAnyWithoutContextPure :: TestConf -> Assertion
testTryAnyWithoutContextPure TestConf { .. } = do
  Left someExn <- tryAnyWithoutContext . runStackT $
    seq (throw TestException) (pure ())
  Just TestException @=? fromException someExn

testTryWithContext :: TestConf -> Assertion
testTryWithContext TestConf { .. } = do
  Left (ErrorWithContext _ctx exn) <- tryWithContext . runStackT $ do
    void $ throwM TestException
    pure ()
  TestException @=? exn

testTryWithContextPure :: TestConf -> Assertion
testTryWithContextPure TestConf { .. } = do
  Left (ErrorWithContext _ctx exn) <- tryWithContext . runStackT $
    seq (throw TestException) (pure ())
  TestException @=? exn

testTryWithoutContext :: TestConf -> Assertion
testTryWithoutContext TestConf { .. } = do
  Left exn <- tryWithoutContext . runStackT $ do
    void $ throwM TestException
    pure ()
  TestException @=? exn

testTryWithoutContextPure :: TestConf -> Assertion
testTryWithoutContextPure TestConf { .. } = do
  Left exn <- tryWithoutContext . runStackT $
    seq (throw TestException) (pure ())
  TestException @=? exn

testContextKv :: TestConf -> Assertion
testContextKv TestConf { .. } = do
  Left (ErrorWithContext ctx TestException) <- tryWithContext . runStackT $
    withErrorContext "ultimate-answer" answer $
    throwM TestException
  HashMap.fromList [("ultimate-answer", toJSON answer)] @=? extractKVs ctx

  where answer :: Int
        answer = 42

testContextKvOverwrite :: TestConf -> Assertion
testContextKvOverwrite TestConf { .. } = do
  Left (ErrorWithContext ctx TestException) <- tryWithContext . runStackT $
    withErrorContext "ultimate-answer" answer $
    withErrorContext "ultimate-answer" answer' $
    throwM TestException
  HashMap.fromList [("ultimate-answer", toJSON answer')] @=? errorContextKVs ctx

  where answer :: Int
        answer = 42

        answer' :: Int
        answer' = 0

testExample :: IO ()
testExample = do
  Left errWithCtx <- tryAnyWithContext . runErrorContextT $
    withErrorNamespace "middle-earth" $
    withErrorNamespace "mordor" $
    withErrorContext "ring-carrier" ("Frodo" :: Text) $
      throwM TestException
  putStrLn . displayException $ errWithCtx