packages feed

hs-bindgen-1.0.0.0: test/common/Test/Common/HsBindgen/Trace/Predicate.hs

-- | Predicates on trace messages
--
-- Intended for unqualified import.
module Test.Common.HsBindgen.Trace.Predicate (
    -- * Predicate
    TraceExpectation (..)
  , TracePredicate -- opaque
  , RenderLabel(..)
  , GotExpectedTrace(..)
  , GotTraceLabelled(..)
  , defaultTracePredicate
  , tolerateAll
  , singleTracePredicate
  , multiTracePredicate
  , multiTracePredicateCustomLogLevel
  , TraceExpectationException
    -- * Tracer
  , quietTracerConfig
  , withTracePredicate
  , withTraceConfigPredicate
  ) where

import Control.Exception (Exception (..), catch, throwIO)
import Control.Monad.Except (Except, runExcept, throwError)
import Data.Foldable qualified as Foldable
import Data.IORef (modifyIORef', newIORef, readIORef)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe)
import Data.String
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Typeable (Typeable)
import Text.SimplePrettyPrint (CtxDoc)
import Text.SimplePrettyPrint qualified as PP

import HsBindgen.Clang
import HsBindgen.Errors
import HsBindgen.Imports (Default (def))
import HsBindgen.IR.C qualified as C
import HsBindgen.Util.Tracer

import Test.Common.HsBindgen.Trace

{-------------------------------------------------------------------------------
  Trace predicates
-------------------------------------------------------------------------------}

data TraceExpectation b = Expected b | Tolerated | Unexpected
  deriving stock (Show, Eq, Ord, Functor)

data TracePredicate l a = TracePredicate {
    _customLogLevel :: CustomLogLevel l a
  , _tracePredicate :: [a] -> Except (TraceExpectationException a) ()
  }

-- | By default, we do not expect any warnings, nor errors ('Unexpected'). Info
-- and debug messages are 'Tolerate'd.
defaultTracePredicate :: IsTrace Level a => TracePredicate Level a
defaultTracePredicate =
    customTracePredicateAux @GotExpectedTrace mempty [] (const Nothing)

-- | Tolerate /all/ traces
--
-- Useful for tests where we don't care about the trace messages, and want to
-- look at test output only.
tolerateAll :: IsTrace Level a => TracePredicate Level a
tolerateAll =
    customTracePredicateAux @GotExpectedTrace mempty [] (const $ Just Tolerated)

-- | Expect a trace with given name exactly one time.
singleTracePredicate ::
     IsTrace Level a
  => (a -> Maybe (TraceExpectation ()))
  -> TracePredicate Level a
singleTracePredicate predicate =
    customTracePredicateAux
      mempty
      [GotExpectedTrace]
      (fmap (fmap (\() -> GotExpectedTrace)) . predicate)

multiTracePredicate :: forall b a.
     (IsTrace Level a, Show a, Ord b, RenderLabel b)
  => [b]
  -- ^ Names/identifiers of expected traces. If a trace is expected N times, add
  -- the name/identifier N times to the list.
  -> (a -> Maybe (TraceExpectation b))
  -- ^ 'Nothing' defaults to 'defaultTracePredicate'.
  -> TracePredicate Level a
multiTracePredicate expected predicate =
    customTracePredicateAux
       mempty
       (map GotTraceLabelled expected)
       (fmap (fmap GotTraceLabelled) . predicate)

-- | Like 'multiTracePredicate' but allows for a custom log level.
multiTracePredicateCustomLogLevel :: forall b a.
     (IsTrace Level a, Show a, Ord b, RenderLabel b)
  => CustomLogLevel Level a
  -> [b]
  -> (a -> Maybe (TraceExpectation b))
  -> TracePredicate Level a
multiTracePredicateCustomLogLevel customLogLevel expected predicate =
    customTracePredicateAux
       customLogLevel
       (map GotTraceLabelled expected)
       (fmap (fmap GotTraceLabelled) . predicate)

-- | Internal generalization
customTracePredicateAux :: forall b a.
     (IsTrace Level a, Ord b, WrongCountMsg a b)
  => CustomLogLevel Level a
  -> [b]
  -> (a -> Maybe (TraceExpectation b))
  -> TracePredicate Level a
customTracePredicateAux customLogLevel names mpredicate =
    TracePredicate customLogLevel $ \traces -> do
      let (unexpected, actualCounts) =
            Foldable.foldl' checkTrace ([], Map.empty) traces
          checkTrace (ts, counts) trace = case predicate trace of
            Expected name -> (ts        , Map.insertWith (<>) name [trace] counts)
            Tolerated     -> (ts        , counts            )
            Unexpected    -> (trace : ts, counts            )
      if null unexpected && expectedCounts == Map.map length actualCounts
        then pure ()
        else
          let additionalCounts = actualCounts `Map.difference` expectedCounts
              additionalWrongCounts = [ wrongCount name 0 (length actual) actual
                                      | (name, actual) <- Map.toList additionalCounts
                                      ]
              wrongCounts = [ wrongCount name expected (length actual) actual
                            | (name, expected) <- Map.toList expectedCounts
                            , let actual = fromMaybe [] (name `Map.lookup` actualCounts)
                            , length actual /= expected
                            ]
           in throwError $ TraceExpectationException {
                  unexpectedTraces = unexpected
                , wrongCounts      = wrongCounts ++ additionalWrongCounts
                }
  where
    getLogLevel :: a -> Level
    getLogLevel x = applyCustomLogLevel customLogLevel x (getDefaultLogLevel x)

    defaultTracePredicateSimple :: a -> TraceExpectation b
    defaultTracePredicateSimple x = case getLogLevel x of
        Error        -> Unexpected
        Bug          -> Unexpected
        Warning      -> Unexpected
        Notice       -> Tolerated
        Info         -> Tolerated
        Debug        -> Tolerated

    predicate :: a -> TraceExpectation b
    predicate trace = fromMaybe (defaultTracePredicateSimple $ trace) (mpredicate trace)

    expectedCounts :: Counter b
    expectedCounts = count names

{-------------------------------------------------------------------------------
  Tracer
-------------------------------------------------------------------------------}

quietTracerConfig :: TracerConfig l a
quietTracerConfig = def {
      outputConfig = OutputConfigCustom OutputCustom{
          report    = noOutput
        , ansiColor = DisableAnsiColor
        }
    }
  where
    noOutput _lvl _trace _traceStr = pure ()

-- | Run an action with a tracer that collects all trace messages.
--
-- Use a t'TracePredicate' to decide whether traces are expected, or unexpected.
withTracePredicate ::
     (Typeable a, IsTrace Level a, Show a)
  => (String -> IO ())
  -> TracePredicate Level a
  -> (Tracer a -> IO b)
  -> IO b
withTracePredicate report predicate action =
    withTraceConfigPredicate report predicate $ \traceConfig ->
      withTracerUnsafe traceConfig (\t _ -> action t)

-- | Run an action with a tracer configuration that collects all trace messages.
--
-- Use a t'TracePredicate' to decide whether traces are expected, or unexpected.
withTraceConfigPredicate ::
     forall a b l. (Typeable a, IsTrace l a, Show a)
  => (String -> IO ())
  -> TracePredicate l a
  -> (TracerConfig l a -> IO b)
  -> IO b
withTraceConfigPredicate report (TracePredicate customLogLevel predicate) action = do
    tracesRef <- newIORef []

    let writer :: Report a
        writer _ trace _ = modifyIORef' tracesRef ((:) trace)

        tracerConfig :: TracerConfig l a
        tracerConfig = def {
            customLogLevel = customLogLevel
            -- The verbosity is @Debug@ so that trace predicates can match on
            -- trace messages of /every/ log level.
          , verbosity      = Verbosity Debug
          , outputConfig   = OutputConfigCustom OutputCustom{
                report    = writer
              , ansiColor = DisableAnsiColor
              }
          }

        reportTraces :: IO ()
        reportTraces = do
          traces <- readIORef tracesRef
          mapM_ (report . show . prettyForTrace) traces


        checkTraces :: IO ()
        checkTraces = do
          traces <- readIORef tracesRef
          case runExcept (predicate traces) of
            Left  e -> throwIO e
            Right _ -> pure ()

    (action tracerConfig <* reportTraces <* checkTraces) `catch` \e ->
      case fromException e of
        Just (_ :: LibclangException) -> reportTraces >> throwIO e
        _                             -> throwIO e

{-------------------------------------------------------------------------------
  Trace exception
-------------------------------------------------------------------------------}

data TraceExpectationException a = TraceExpectationException {
      unexpectedTraces :: [a]
    , wrongCounts      :: [CtxDoc]
    }

instance (IsTrace l a, Show a) => Show (TraceExpectationException a) where
  show e = show $
      PP.vcat $
           ( if null e.unexpectedTraces
               then []
               else "Unexpected traces:"
                  : map reportTrace e.unexpectedTraces
                  ++ ["\n"]
           )
        ++ ( if null e.wrongCounts
               then []
               else "Expected traces with wrong counts:"
                  : e.wrongCounts
                  ++ ["\n"]
           )


instance (Typeable a, IsTrace l a, Show a)
  => Exception (TraceExpectationException a)

{-------------------------------------------------------------------------------
  Wrong counts
-------------------------------------------------------------------------------}

-- | Emit message about unexpected traces
--
-- Given a particular type of trace message (say, @a = TraceMsg@), we typically
-- distill some info from this message (say, @b = C.DeclName@), and then compare
-- that info against a list of expected info. There are then two main cases
-- (that is, main choses for @b@):
--
-- * t'GotExpectedTrace': we either expect the trace message, or we don't; we
--   don't expect to get more than one.
-- * t'GotTraceLabelled': we expect multiple traces for this case, and label each
--   one. In this case, we show specific counts.
--
-- This is an internal class (not exported).
class WrongCountMsg a b where
  wrongCount ::
       b    -- ^ Name
    -> Int  -- ^ Expected count
    -> Int  -- ^ Actual count
    -> [a]  -- ^ List of traces
    -> CtxDoc

-- | See 'WrongCountMsg' for discussion
data GotExpectedTrace = GotExpectedTrace
  deriving stock (Show, Eq, Ord)

-- | See 'WrongCountMsg' for discussion
newtype GotTraceLabelled b = GotTraceLabelled b
  deriving stock (Show, Eq, Ord)
  deriving newtype (IsString)

-- | The most common case: traces with just one outcome
instance IsTrace l a => WrongCountMsg a GotExpectedTrace where
  wrongCount _ 1 n _ =
      case compare n 1 of
        LT -> "Expected a single trace but no trace was emitted"
        EQ -> panicPure "Received correct count"
        GT -> "Expected a single trace but more traces were emitted"
  wrongCount _ i _ _ =
      panicPure $ "Unexpected \"expected count\": " ++ show i

-- | The general case, with user-defined labels as documents
instance (IsTrace l a, Show a, RenderLabel b)
      => WrongCountMsg a (GotTraceLabelled b) where
  wrongCount (GotTraceLabelled label) expectedCount actualCount traces =
      PP.hangs' intro 2 $ map reportTrace traces
    where
      intro = PP.hcat
        [ "Name: ",             renderLabel label
        , ", expected count: ", PP.show expectedCount
        , ", actual count: "  , PP.show actualCount
        ]

{-------------------------------------------------------------------------------
  Auxiliary to 'GotTraceLabelled': render labels
-------------------------------------------------------------------------------}

class RenderLabel b where
  renderLabel :: b -> CtxDoc

  default renderLabel :: PrettyForTrace b => b -> CtxDoc
  renderLabel = prettyForTrace

instance RenderLabel CtxDoc where renderLabel = id
instance RenderLabel String where renderLabel = PP.string
instance RenderLabel Text   where renderLabel = PP.string . Text.unpack

instance RenderLabel C.DeclName
instance RenderLabel C.ScopedName

instance RenderLabel a => RenderLabel (Maybe a) where
  renderLabel Nothing  = ""
  renderLabel (Just x) = renderLabel x

instance RenderLabel () where
  renderLabel () = "()"
instance ( RenderLabel a
         , RenderLabel b
         ) => RenderLabel (a, b) where
  renderLabel (x, y) = PP.parens $ PP.hlist "(" ")" [
        renderLabel x
      , renderLabel y
      ]
instance ( RenderLabel a
         , RenderLabel b
         , RenderLabel c
         ) => RenderLabel (a, b, c) where
  renderLabel (x, y, z) = PP.parens $ PP.hlist "(" ")" [
        renderLabel x
      , renderLabel y
      , renderLabel z
      ]

{-------------------------------------------------------------------------------
  Counter
-------------------------------------------------------------------------------}

type Counter a = Map a Int

addN :: (Ord a) => Int -> Counter a -> a -> Counter a
addN n m k = Map.insertWith (const (+ n)) k n m

count :: (Foldable f, Ord a) => f a -> Counter a
count = Foldable.foldl' (addN 1) Map.empty