packages feed

log4hs-0.0.5.0: test/LoggingSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE TemplateHaskell   #-}

module LoggingSpec ( spec ) where

import           Control.Concurrent.MVar
import           Control.Monad
import           Data.Default            (def)
import           Data.List               (intercalate)
import qualified Data.Map                as M
import           Data.Word
import           Prelude                 hiding (error)
import           System.IO
import           System.IO.Unsafe        (unsafePerformIO)
import           System.Process          (createPipe)
import           Test.Hspec
import           Test.Hspec.QuickCheck
import           Test.QuickCheck         hiding (run)
import qualified Test.QuickCheck.Monadic as Q

import           Logging

spec :: Spec
spec = basicSpec >> formatSpec >> filterSpec >> propagateSpec


basicSpec :: Spec
basicSpec = describe "Basic" $ modifyMaxSize (const 1000) $ do
  prop "log to root" $ \(PrintableString msg) ->
    runLog "" "DEBUG" msg "debug" (== msg)
  prop "log to Log4hs" $ \(PrintableString msg) ->
    runLog "Log4hs" "INFO" msg "debug" (== msg)
  prop "log to Log4hs.Json" $ \(PrintableString msg) ->
    runLog "Log4hs.Json" "WARN" msg "debug" (== msg)
  prop "disabled some" $ \(PrintableString msg) level ->
    runLog "Log10000hs" level msg "debug" (== "")
  prop "disabled all" $ \(PrintableString msg) loggers level ->
    let logger = intercalate "." $ [getPrintableString x | x <- loggers]
    in runDisabledLog logger level msg


formatSpec :: Spec
formatSpec = describe "Format" $ modifyMaxSize (const 1000) $ do
  prop "simple" $ \(PrintableString msg) ->
    runLog "" "DEBUG" msg "debug" (== msg)
  prop "logger" $ \(PrintableString msg) (Log4hsLogger logger) -> do
    runLog logger "WARN" msg "info" (== logger ++ ": " ++ msg)
  prop "level" $ \(PrintableString msg) (Log4hsLogger logger) fmtlevel ->
    let (Log4hsHighLevel level) = fmtlevel
        out = logger ++ " " ++ (show level) ++ "] " ++ msg
    in runLog logger level msg "error" (== out)


filterSpec :: Spec
filterSpec = describe "Filter" $ modifyMaxSize (const 1000) $ do
  prop "root sink && debug handler" $ \(PrintableString msg) level ->
    runLog "" level msg "debug" $ (== if level >= "DEBUG" then msg else "")
  prop "Log4hs sink && info handler" $ \(PrintableString msg) level ->
    let out = if level >= "INFO" then "Log4hs: " ++ msg else ""
    in runLog "Log4hs" level msg "info" $ (== out)
  prop "Log4hs.Json sink && error handler" $ \(PrintableString msg) level ->
    let out = if level >= "ERROR"
                then "Log4hs.Json " ++ (show level) ++ "] " ++ msg
                else ""
    in runLog "Log4hs.Json" level msg "error" $ (== out)
  prop "handler's filterer" $ \(PrintableString msg) (Small v)->
    let logger = "Log4hs.V" ++ (show v)
        out = if v == (3 :: Word8) then logger ++ ": " ++ msg else ""
    in runLog logger "INFO" msg "infov3" (== out)


propagateSpec :: Spec
propagateSpec = describe "Propagate" $ modifyMaxSize (const 1000) $ do
  prop "to root (real)" $ \(PrintableString msg) ->
    runLog "Log8hs" "WARN" msg "debug" $ (== msg)
  prop "to root (virtual)" $ \(PrintableString msg) ->
    runLog "Log0hs" "WARN" msg "debug" $ (== msg)
  prop "to parent (real)" $ \(PrintableString msg) ->
    runLog "Log4hs.Json" "WARN" msg "info" $ (== "Log4hs.Json: " ++ msg)
  prop "to parent (virtual)" $ \(PrintableString msg) ->
    runLog "Log4hs.Ini" "WARN" msg "info" $ (== "Log4hs.Ini: " ++ msg)
  prop "to grandparent (real)" $ \(PrintableString msg) ->
    runLog "Log8hs.Json" "WARN" msg "debug" $ (== msg)
  prop "to grandparent (virtual)" $ \(PrintableString msg) ->
    runLog "Log0hs.Json" "WARN" msg "debug" $ (== msg)
  prop "disabled" $ \(PrintableString msg) ->
    runLog "Log4hs" "FATAL" msg "fatal" $ (== "")


runLog :: Logger -> Level -> String -> String -> (String -> Bool) -> Property
runLog logger level msg stream test = Q.monadicIO $ do
  out <- Q.run $ createManager False >>= \(mgr, streams) -> run mgr $
    $(logv) logger level msg  >> hTryGetLine (streams M.! stream)
  Q.assert $ test out


runDisabledLog :: Logger -> Level -> String -> Property
runDisabledLog logger level msg = Q.monadicIO $ do
  outs <- Q.run $ createManager True >>= \(mgr, streams) -> run mgr $
    $(logv) logger level msg  >> sequence (M.map hTryGetLine streams)
  Q.assert $ M.empty == M.filter (/= "") outs


createManager :: Bool -> IO (Manager, M.Map String Handle)
createManager disabled = do
  debugPh <- createPipeHandler "DEBUG" [] (formatters M.! "simple")
  infoPh <- createPipeHandler "INFO" [] (formatters M.! "logger")
  infov3Ph <- createPipeHandler "INFO" ["Log4hs.V3"] (formatters M.! "logger")
  errorPh <- createPipeHandler "ERROR" [] (formatters M.! "level")
  fatalPh <- createPipeHandler "FATAL" [] (formatters M.! "logger")

  let root = Sink "" "DEBUG" [] [fst debugPh, fst fatalPh] False False
      handlers = [fst debugPh, fst infoPh, fst infov3Ph, fst errorPh]
      log4hs = Sink "Log4hs" "INFO" [] handlers False False
      log4hsJson = Sink "Log4hs.Json" "WARN" [] [] False True
      log4hsYaml = Sink "Log4hs.Yaml" "WARN" [] [] False True
      log8hs = Sink "Log8hs" "INFO" [] [] False True
      log8hsJson = Sink "Log8hs.Json" "INFO" [] [] False True
      log10000hs = Sink "Log8hs.Json" "INFO" [] [fst debugPh] True False
      sinks = M.fromList [ ("Log4hs", log4hs)
                         , ("Log4hs.Json", log4hsJson)
                         , ("Log4hs.Yaml", log4hsYaml)
                         , ("Log8hs", log8hs)
                         , ("Log8hs.Json", log8hsJson)
                         , ("Log10000hs", log10000hs)
                         ]
      streams = M.fromList $ [ ("debug", snd debugPh)
                             , ("info", snd infoPh)
                             , ("infov3", snd infov3Ph)
                             , ("error", snd errorPh)
                             , ("fatal", snd fatalPh)
                             ]
  return (Manager root sinks disabled False, streams)


formatters :: M.Map String Formatter
formatters = M.fromList
  [ ("simple", def {fmt = "%(message)s"})
  , ("logger", def {fmt = "%(logger)s: %(message)s"})
  , ("level", def {fmt = "%(logger)s %(level)s] %(message)s"})
  ]


createPipeHandler :: Level -> Filterer -> Formatter -> IO (HandlerT, Handle)
createPipeHandler level filterer formatter = do
  lock <- newMVar ()
  (read, write) <- createPipe
  hSetEncoding read utf8
  hSetEncoding write utf8
  return $ ( HandlerT $ StreamHandler write level filterer formatter lock
           , read
           )


hTryGetLine :: Handle -> IO String
hTryGetLine hdl = do
  ready <- hReady hdl
  if ready then hGetLine hdl else return ""


instance Arbitrary Level where
  arbitrary = Level <$> arbitrary


newtype Log4hsLogger = Log4hsLogger Logger deriving Show

instance Arbitrary Log4hsLogger where
  arbitrary = elements $
    map Log4hsLogger ["Log4hs", "Log4hs.Json", "Log4hs.Yaml"]


newtype Log4hsHighLevel = Log4hsHighLevel Level deriving Show

instance Arbitrary Log4hsHighLevel where
  arbitrary = elements $ map Log4hsHighLevel ["ERROR", "FATAL", Level 100]