katip-0.7.0.0: test/Katip/Tests.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Katip.Tests
( tests
) where
-------------------------------------------------------------------------------
import Control.Applicative as A
import Control.Concurrent.STM
import Control.Exception.Safe
import Data.Aeson
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import Data.Monoid as Monoid
import Data.Text (Text)
import qualified Data.Text.Lazy.Builder as B
import Data.Time
import Data.Time.Clock.POSIX
import Language.Haskell.TH
import Lens.Micro (ASetter, (&), (.~))
import System.Posix.Types
import Test.QuickCheck.Instances ()
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
-------------------------------------------------------------------------------
import Katip
import Katip.Core
-------------------------------------------------------------------------------
tests :: TestTree
tests = testGroup "Katip"
[
testProperty "JSON cycle Item" $ \(i :: Item ()) ->
prop_json_cycle i
, testProperty "JSON cycle verbosity" $ \(v :: Verbosity) ->
prop_json_cycle v
, eqItemTests
, testProperty "renderSeverity/textToSeverity cycle" $ \sev ->
textToSeverity(renderSeverity sev) === Just sev
, testProperty "processIDToText/textToProcessID cycle" $ \pid ->
textToProcessID (processIDToText pid) === Just pid
, testCase "processIDToText is just the number" $ do
processIDToText 123 @?= "123"
, logContextsTests
, closeScribeTests
, closeScribesTests
, loggingTests
]
-------------------------------------------------------------------------------
logContextsTests :: TestTree
logContextsTests = testGroup "logContexts"
[
testCase "overwrites with the right-hand side (right-bias)" $ do
let l1 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("a" :: Text))])
l2 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("b" :: Text))])
l3 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("c" :: Text))])
both = l1 <> l2 <> l3
toObject both @?= HM.singleton "foo" (String "c")
, testCase "respects payloadKeys for each constituent payload" $ do
let everything = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("a" :: Text))])
conservative = liftPayload (ConservativePayload "always" "rarely")
both = everything <> conservative
payloadKeys V2 both @?= SomeKeys ["often_shown", "rarely_shown", "foo"]
payloadKeys V1 both @?= SomeKeys ["often_shown", "foo"]
]
-------------------------------------------------------------------------------
closeScribeTests :: TestTree
closeScribeTests = testGroup "closeScribe"
[ testCase "removes the specified scribe" $ do
(scr, finalizerCalled) <- trivialScribe
le <- registerScribe "trivial" scr defaultScribeSettings =<< initLogEnv "ns" "test"
le' <- closeScribe "trivial" le
closed <- atomically (readTVar finalizerCalled)
assertBool "finalizer called" closed
assertBool "should not have trivial key in scribes" (not (M.member "trivial" (_logEnvScribes le')))
, testCase "does nothing for a missing scribe" $ do
le <- initLogEnv "ns" "test"
le' <- closeScribe "nah" le
assertBool "does not affect scribes" (M.null (_logEnvScribes le'))
, testCase "re-throws finalizer exceptions" $ do
(scr, finalizerCalled) <- brokenScribe 1
le <- registerScribe "broken" scr defaultScribeSettings =<< initLogEnv "ns" "test"
res <- try (closeScribe "broken" le)
closed <- atomically (readTVar finalizerCalled)
assertBool "finalizer called" closed
case res of
Left (ScribeBroken scribeNo) -> scribeNo @?= 1
Right _ -> assertFailure "Expected to throw a ScribeBroken but it did not"
]
-------------------------------------------------------------------------------
loggingTests :: TestTree
loggingTests = testGroup "logging"
[ testCase "logs in order with contexts, namespaces, etc" $ do
(le, items) <- recordingEnv
runKatipContextT le (sl "base_context" (42 :: Int)) "base_namespace" $ do
$(logTM) InfoS "basic log"
katipNoLogging $ do
$(logTM) InfoS "you cant see this"
katipAddNamespace "added" $ do
katipAddNamespace "namespace" $ do
$(logTM) InfoS "with namespaces"
katipAddContext (sl "additional" True) $ do
$(logTM) InfoS "additional context"
_ <- closeScribes le
summary <- fmap summarizeItem <$> readTVarIO items
let baseCtx = HM.singleton "base_context" (Number 42)
let baseNS = "tests" <> "base_namespace"
summary @?=
[ (baseNS, baseCtx, "basic log")
, (baseNS <> "added" <> "namespace", baseCtx, "with namespaces")
, (baseNS <> "added", HM.insert "additional" (Bool True) baseCtx, "additional context")
]
, testCase "Katip.Monadic.logLocM" $ do
(le, items) <- recordingEnv
runKatipContextT le (sl "base_context" (42 :: Int)) "base_namespace" $ logLocM InfoS "basic log"
_ <- closeScribes le
loggedItems <- readTVarIO items
let loc = _itemLoc (head loggedItems)
fmap loc_module loc @?= Just "Katip.Tests"
]
where
recordingEnv :: IO (LogEnv, TVar [Item Object])
recordingEnv = do
items <- newTVarIO Monoid.mempty
let scribe = Scribe
{ liPush = \i -> atomically (modifyTVar' items (<> [toObject <$> i]))
, scribeFinalizer = return ()
}
le1 <- initLogEnv "tests" "test"
le2 <- registerScribe "recorder" scribe defaultScribeSettings le1
return (le2, items)
summarizeItem :: Item Object -> (Namespace, Object, LogStr)
summarizeItem Item {..} = (_itemNamespace, _itemPayload, _itemMessage)
-------------------------------------------------------------------------------
trivialScribe :: IO (Scribe, TVar Bool)
trivialScribe = do
finalizerCalled <- newTVarIO False
let finalizer = atomically (writeTVar finalizerCalled True)
return (Scribe (const (return ())) finalizer, finalizerCalled)
-------------------------------------------------------------------------------
brokenScribe :: Int -> IO (Scribe, TVar Bool)
brokenScribe scribeNum = do
finalizerCalled <- newTVarIO False
let finalizer = do
atomically (writeTVar finalizerCalled True)
throw (ScribeBroken scribeNum)
return (Scribe (const (return ())) finalizer, finalizerCalled)
-------------------------------------------------------------------------------
data BrokenScribeError = ScribeBroken Int deriving (Show, Typeable)
instance Exception BrokenScribeError
-------------------------------------------------------------------------------
closeScribesTests :: TestTree
closeScribesTests = testGroup "closeScribes"
[ testCase "returns a log env with no scribes" $ do
(scr, finalizerCalled) <- trivialScribe
le <- registerScribe "trivial" scr defaultScribeSettings =<< initLogEnv "ns" "test"
le' <- closeScribes le
closed <- atomically (readTVar finalizerCalled)
assertBool "finalizer called" closed
assertBool "remvoes all scribes" (M.null (_logEnvScribes le'))
, testCase "throws the first exception encountered after closing all scribes" $ do
(scr1, finalizerCalled1) <- brokenScribe 1
(scr2, finalizerCalled2) <- brokenScribe 2
le <- registerScribe "broken2" scr2 defaultScribeSettings =<< registerScribe "broken1" scr1 defaultScribeSettings =<< initLogEnv "ns" "test"
res <- try (closeScribes le)
closed1 <- atomically (readTVar finalizerCalled1)
assertBool "finalizer 1 called" closed1
closed2 <- atomically (readTVar finalizerCalled2)
assertBool "finalizer 2 called" closed2
case res of
Left (ScribeBroken scribeNo) -> scribeNo @?= 1
Right _ -> assertFailure "Expected to throw a ScribeBroken but it did not"
]
-------------------------------------------------------------------------------
data ConservativePayload = ConservativePayload {
oftenShown :: Text
, rarelyShown :: Text
}
instance ToJSON ConservativePayload where
toJSON ConservativePayload {..} = object ["often_shown" .= oftenShown
,"rarely_shown" .= rarelyShown]
instance ToObject ConservativePayload
instance LogItem ConservativePayload where
payloadKeys V1 _ = SomeKeys ["often_shown"]
payloadKeys V0 _ = SomeKeys []
payloadKeys _ _ = AllKeys
-------------------------------------------------------------------------------
prop_json_cycle :: (ToJSON a, FromJSON a, Eq a, Show a) => a -> Property
prop_json_cycle a = eitherDecode (encode a) === Right a
-------------------------------------------------------------------------------
instance Arbitrary a => Arbitrary (Item a) where
arbitrary = Item
A.<$> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> (getCleanUTCTime <$> arbitrary)
<*> arbitrary
<*> arbitrary
-------------------------------------------------------------------------------
newtype CleanUTCTime = CleanUTCTime {
getCleanUTCTime :: UTCTime
}
-------------------------------------------------------------------------------
-- Work around time parsing precision issues in aeson
instance Arbitrary CleanUTCTime where
arbitrary = CleanUTCTime . posixSecondsToUTCTime . fromInteger <$> arbitrary
-------------------------------------------------------------------------------
deriving instance Arbitrary Namespace
deriving instance Arbitrary Environment
deriving instance Arbitrary ThreadIdText
deriving instance Arbitrary CPid
-------------------------------------------------------------------------------
instance Arbitrary Loc where
arbitrary = do
f <- arbitrary
p <- arbitrary
m <- arbitrary
s <- arbitrary
return $ Loc f p m s s
-------------------------------------------------------------------------------
instance Arbitrary Verbosity where
arbitrary = oneof $ map pure [V0, V1, V2, V3]
-------------------------------------------------------------------------------
instance Arbitrary Severity where
arbitrary = oneof $ map pure [ DebugS
, InfoS
, NoticeS
, WarningS
, ErrorS
, CriticalS
, AlertS
, EmergencyS
]
-------------------------------------------------------------------------------
instance Arbitrary LogStr where
arbitrary = LogStr . B.fromText <$> arbitrary
-------------------------------------------------------------------------------
-- Somewhat test whether all fields are taken into account in `==`
#if !MIN_VERSION_template_haskell(2, 10, 0)
-- `testProperty` requires these instances
deriving instance Eq Loc
deriving instance Show Loc
#endif
eqItemTests :: TestTree
eqItemTests = testGroup "Eq Item"
[ testProperty "itemApp" $ prop_field itemApp
, testProperty "itemEnv" $ prop_field itemEnv
, testProperty "itemSeverity" $ prop_field itemSeverity
, testProperty "itemThread" $ prop_field itemThread
, testProperty "itemHost" $ prop_field itemHost
, testProperty "itemProcess" $ prop_field itemProcess
, testProperty "itemPayload" $ prop_field itemPayload
, testProperty "itemMessage" $ prop_field itemMessage
, testProperty "itemTime" $ prop_field itemTime
, testProperty "itemNamespace" $ prop_field itemNamespace
, testProperty "itemLoc" $ prop_field itemLoc
]
where
prop_field :: Eq a => ASetter (Item ()) (Item ()) a a -> Item () -> a -> a -> Bool
prop_field field item f1 f2 =
((item & field .~ f1) == (item & field .~ f2)) == (f1 == f2)