packages feed

log4hs-0.1.0.0: test/Logging/AesonSpec.hs

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

module Logging.AesonSpec ( spec ) where

import           Control.Lens                (view)
import           Control.Monad
import           Data.Aeson
#if MIN_VERSION_aeson(1, 4, 3)
import           Data.Aeson.QQ.Simple        (aesonQQ)
#else
import           Data.Aeson.QQ               (aesonQQ)
#endif
import           Data.Default                (def)
import           Data.Generics.Product.Typed
import           Data.List                   (intercalate)
import qualified Data.Map                    as M
import           Data.Maybe                  (fromJust)
import           System.IO
import           System.IO.Unsafe
import           Test.Hspec
import           Test.Hspec.QuickCheck

import           Logging
import           Logging.Aeson

spec :: Spec
spec = levelSpec >> filterSpec >> formatterSpec >> handlerSpec >>
       sinkSpec >>  managerSpec


levelSpec :: Spec
levelSpec = describe "Level" $ modifyMaxSize (const 1000) $ do
  prop "decode" $ \x -> let v = Level x in (decode $ encode $ show v) == Just v


filterSpec :: Spec
filterSpec = describe "Filter" $ modifyMaxSize (const 1000) $ do
  prop "decode" $ \fs ->
    let logger = intercalate "." fs
    in (decode $ encode logger) == Just (Filter logger)


formatterSpec :: Spec
formatterSpec = describe "Formatter" $ modifyMaxSize (const 1000) $ do
  prop "decode standard" $ \(fmt, datefmt) ->
    let formatter = Formatter fmt datefmt
        obj = object [("fmt", toJSON fmt), ("datefmt", toJSON datefmt)]
    in (decode $ encode obj) == Just formatter

  prop "decode missing filed" $ \fmt ->
    decode (encode $ object [("fmt", toJSON fmt)]) == Just (def {fmt = fmt})

  prop "decode string" $ \fmt -> decode (encode fmt) == Just (def {fmt = fmt})


handlerSpec :: Spec
handlerSpec = describe "Handler" $ modifyMaxSize (const 1000) $ do
  it "decode StreamHandler simple" $ do
    let StreamHandler{..} = fromJust $ decode $ encode $
          [aesonQQ|{"type": "StreamHandler"}|]
    stream == stderr `shouldBe` True
    level == def `shouldBe` True
    filterer == [] `shouldBe` True
    formatter == def `shouldBe` True

  it "decode StreamHandler standard" $ do
    let StreamHandler{..} = fromJust $ decode $ encode $
          [aesonQQ|
            {
              "type": "StreamHandler",
              "stream": "stdout",
              "level": "DEBUG",
              "filterer": ["Module.Submodule"],
              "formatter": "default"
            }
          |]

    stream == stdout `shouldBe` True
    level == "DEBUG" `shouldBe` True
    filterer == [Filter "Module.Submodule"] `shouldBe` True
    formatter == def {fmt = "default"} `shouldBe` True

  it "decode FileHandler" $ do
    FileHandler{..} <- fromJust $ decode $ encode $
          [aesonQQ|
            {
              "type": "FileHandler",
              "file": "myapp.log",
              "encoding": "UTF-16",
              "level": "INFO",
              "filterer": ["Module.Submodule"],
              "formatter": "default"
            }
          |]

    file `shouldBe` "myapp.log"
    encoding `shouldBe` utf16
    level `shouldBe` "INFO"
    filterer == [Filter "Module.Submodule"] `shouldBe` True
    formatter == def {fmt = "default"} `shouldBe` True

  it "decode (formatters -> SomeHandler)" $ do
    let simple = def {fmt = "%(logger)s: %(message)s"}
        func = fromJust $ decode $ encode
          [aesonQQ|
            {
              "type": "StreamHandler",
              "level": "DEBUG",
              "filterer": ["Module.Submodule"],
              "formatter": "simple"
            }
          |]

    handler@(SomeHandler _) <- func $ M.singleton ("simple" :: String) simple
    view (typed @Level) handler == "DEBUG" `shouldBe` True
    view (typed @Filterer) handler == ["Module.Submodule"] `shouldBe` True
    view (typed @Formatter) handler == simple `shouldBe` True


sinkSpec :: Spec
sinkSpec = describe "Sink" $ modifyMaxSize (const 1000) $ do
  it "decode" $ do
    let Sink{..} = fromJust $ decode $ encode $
          [aesonQQ|
            {
              "level": "DEBUG",
              "filterer": ["Module.Submodule"],
              "handlers": ["console"],
              "disabled": false,
              "propagate": true
            }
          |]

    logger `shouldBe` "placeholder"
    level `shouldBe` "DEBUG"
    filterer == ["Module.Submodule"] `shouldBe` True
    length handlers `shouldBe` 0
    disabled `shouldBe` False
    propagate `shouldBe` True

  it "decode (logger -> handlers -> sink)" $ do
    let handlerMap = M.fromList [ ("console", toHandler stderrHandler)
                                , ("file" :: String, toHandler stderrHandler)
                                ]
        func = fromJust $ decode $ encode $
              [aesonQQ|
                {
                  "level": "INFO",
                  "filterer": ["Module.Submodule"],
                  "handlers": ["console", "file"]
                }
              |]
        Sink{..} = func ("MyLogger" :: String) handlerMap

    logger `shouldBe` "MyLogger"
    level `shouldBe` "INFO"
    filterer == ["Module.Submodule"] `shouldBe` True
    length handlers `shouldBe` 2 -- deep test handlers
    disabled `shouldBe` False
    propagate `shouldBe` False


managerSpec :: Spec
managerSpec = describe "Manager" $ modifyMaxSize (const 1000) $ do
  it "decode" $ do
    let Manager{..} = manager_

    length sinks `shouldBe` 1
    M.member "MyLogger" sinks `shouldBe` True
    disabled `shouldBe` False
    catchUncaughtException `shouldBe` True

  it "decode root" $ do
    let Sink{..} = root manager_

    logger `shouldBe` ""
    level `shouldBe` "DEBUG"
    disabled `shouldBe` False
    propagate `shouldBe` False
    length filterer `shouldBe` 0
    length handlers `shouldBe` 1

  it "decode sinks" $ do
    let Sink{..} = (sinks manager_) M.! "MyLogger"

    logger `shouldBe` "MyLogger"
    level `shouldBe` "INFO"
    propagate `shouldBe` False
    disabled `shouldBe` False
    length filterer `shouldBe` 1
    filterer == ["MyLogger.Main"] `shouldBe` True
    length handlers `shouldBe` 2


-- Decoding manager globally, or it will throw file resouse busy error
manager_ :: Manager
{-# NOINLINE manager_ #-}
manager_ = unsafePerformIO $ fromJust $ decode $ encode [aesonQQ|{
  "catchUncaughtException": true,
  "loggers": {
      "root": {
        "level": "DEBUG",
        "handlers": ["console"],
        "propagate": false
      },
      "MyLogger": {
        "level": "INFO",
        "filterer": ["MyLogger.Main"],
        "handlers": ["console", "file"],
        "propagate": false
      }
    },
    "handlers": {
      "console": {
        "type": "StreamHandler",
        "stream": "stderr",
        "level": "DEBUG",
        "formatter": "default"
      },
      "file": {
        "type": "FileHandler",
        "level": "INFO",
        "formatter": "default",
        "file": "./default.log"
      }
    },
    "formatters": {
      "default": {
        "fmt": "%(asctime)s - %(level)s - %(logger)s:%(lineno)d] %(message)s"
      }
    }
}|]