packages feed

cleveland-0.3.0: src/Test/Cleveland/Internal/Exceptions/CallStack.hs

-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

{-# OPTIONS_HADDOCK not-home #-}

-- | This module defines 'CallStackAnnotation' and required helpers and
-- instances to use it with 'ExceptionAnnotation'.
module Test.Cleveland.Internal.Exceptions.CallStack
  ( CallStackAnnotation(..)
  , addCallStack
  ) where

import Fmt (build, unlinesF)
import GHC.Exception (SrcLoc(..), fromCallSiteList)
import GHC.Stack (popCallStack)

import Test.Cleveland.Internal.Exceptions.Annotated

-- | Newtype wrapper for 'CallStack' to avoid orphan instances.
--
-- Has instance of 'ExceptionAnnotation'.
newtype CallStackAnnotation = CallStackAnnotation { unCallStackAnnotation :: CallStack }
  deriving stock Show

-- | A convenience synonym for @annotateExceptions (CallStackAnnotation callStack)@.
addCallStack :: (MonadCatch m, HasCallStack) => m a -> m a
addCallStack = annotateExceptions (CallStackAnnotation $ popCallStack callStack)
-- NB: 'popCallStack' is required to hide 'addCallStack' itself from the stack.

instance ExceptionAnnotation CallStackAnnotation where
  displayAnnotation cs acc = unlinesF
    [ acc
    , ""
    , build $ prettyCallStack $ unCallStackAnnotation cs
    ]
  annotationPriority = -1000
  -- NB: we want call stacks to be basically printed last.

instance Semigroup CallStackAnnotation where
  a <> b = CallStackAnnotation $ unCallStackAnnotation a `mergeCallStacks` unCallStackAnnotation b

-- | Merge two 'CallStack's, removing duplicates, but keeping the ordering.
mergeCallStacks :: CallStack -> CallStack -> CallStack
mergeCallStacks a b = fromCallSiteList
  $ fmap (fmap unOrdSrcLoc)
  $ ordNub
  $ fmap (fmap OrdSrcLoc)
  $ getCallStack a <> getCallStack b

newtype OrdSrcLoc = OrdSrcLoc { unOrdSrcLoc :: SrcLoc }
  deriving newtype Eq

instance Ord OrdSrcLoc where
  compare (OrdSrcLoc (SrcLoc a b c d e f g)) (OrdSrcLoc (SrcLoc a' b' c' d' e' f' g'))
    = compare (a, b, c, d, e, f, g) (a', b', c', d', e', f', g')