2022-02-23 22:32:18 +03:00
|
|
|
{-# OPTIONS -Wno-redundant-constraints #-}
|
|
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
|
|
|
|
|
|
-- | A replacement for "Control.Exception" with a few extra helpers.
|
|
|
|
module Harness.Exceptions
|
|
|
|
( catchRethrow,
|
|
|
|
tryInOrder,
|
2022-04-04 17:45:12 +03:00
|
|
|
forFinally_,
|
2022-06-08 19:35:44 +03:00
|
|
|
rethrowAll,
|
2022-03-15 19:08:47 +03:00
|
|
|
module GHC.Stack,
|
2022-02-23 22:32:18 +03:00
|
|
|
module Control.Exception.Safe,
|
|
|
|
Exceptions (..),
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Control.Exception.Safe
|
2022-06-08 19:35:44 +03:00
|
|
|
import Data.List.NonEmpty qualified as NE
|
2022-02-23 22:32:18 +03:00
|
|
|
import Data.String
|
|
|
|
import GHC.Stack
|
|
|
|
import Hasura.Prelude hiding (first)
|
|
|
|
|
|
|
|
-- | Runs an action with a handler.
|
|
|
|
--
|
|
|
|
-- Will run the action. If the action fails and throws an exception,
|
|
|
|
-- it will run the cleanup and will throw the original exception after it is done.
|
|
|
|
-- If the cleanup fails as well, it will throw both raised exceptions.
|
2022-06-08 19:35:44 +03:00
|
|
|
catchRethrow :: HasCallStack => IO a -> IO () -> IO a
|
2022-02-23 22:32:18 +03:00
|
|
|
catchRethrow action cleanup =
|
|
|
|
catch
|
|
|
|
-- attempt action
|
|
|
|
action
|
|
|
|
( \actionEx -> do
|
|
|
|
-- try to cleanup the action
|
|
|
|
-- if clean also fails, throw both errors
|
2022-06-08 19:35:44 +03:00
|
|
|
_ <- catch cleanup (throwIO . Exceptions . (actionEx NE.:|) . (: []))
|
2022-02-23 22:32:18 +03:00
|
|
|
-- if clean succeeds, throw the original error
|
|
|
|
throwIO actionEx
|
|
|
|
)
|
|
|
|
|
|
|
|
-- | Try actions in order. If one succeeds, it succeeds. If both fail, throw both exceptions.
|
|
|
|
tryInOrder :: HasCallStack => IO a -> IO a -> IO a
|
|
|
|
tryInOrder action1 action2 =
|
|
|
|
catch
|
|
|
|
action1
|
|
|
|
( \action1Ex ->
|
|
|
|
catch
|
|
|
|
action2
|
2022-06-08 19:35:44 +03:00
|
|
|
(throwIO . Exceptions . (\action2Ex -> action1Ex NE.:| [action2Ex]))
|
2022-02-23 22:32:18 +03:00
|
|
|
)
|
|
|
|
|
2022-04-04 17:45:12 +03:00
|
|
|
-- | Like 'for_', but uses 'finally' instead of '*>' to make sure all actions run even when
|
|
|
|
-- an exception occurs. Will throw the first error it runs into.
|
|
|
|
forFinally_ :: [a] -> (a -> IO ()) -> IO ()
|
|
|
|
forFinally_ list f =
|
|
|
|
case list of
|
|
|
|
[] -> pure ()
|
|
|
|
x : xs -> f x `finally` forFinally_ xs f
|
|
|
|
|
2022-06-08 19:35:44 +03:00
|
|
|
-- | Run a list of IO actions, collecting and rethrowing all exceptions that are
|
|
|
|
-- raised as a single 'Exceptions' exception. If 'Exceptions' thrown in the
|
|
|
|
-- 'actions', these are collapsed into a single top-level 'Exceptions'
|
|
|
|
-- exception.
|
|
|
|
rethrowAll :: HasCallStack => [IO ()] -> IO ()
|
|
|
|
rethrowAll actions = do
|
|
|
|
exns <-
|
|
|
|
concat
|
|
|
|
<$> mapM
|
|
|
|
(\action -> handle (return . collectExns) ([] <$ action))
|
|
|
|
actions
|
|
|
|
case NE.nonEmpty exns of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just (ex NE.:| []) -> throwIO ex
|
|
|
|
Just exnsNE -> throwIO (Exceptions exnsNE)
|
|
|
|
where
|
|
|
|
collectExns :: SomeException -> [SomeException]
|
|
|
|
collectExns exn | Just (Exceptions exns) <- fromException exn = concatMap collectExns exns
|
|
|
|
collectExns exn = [exn]
|
|
|
|
|
2022-02-23 22:32:18 +03:00
|
|
|
-- | Two exceptions, bundled as one.
|
|
|
|
data Exceptions
|
2022-06-08 19:35:44 +03:00
|
|
|
= Exceptions (NE.NonEmpty SomeException)
|
2022-02-23 22:32:18 +03:00
|
|
|
deriving anyclass (Exception)
|
|
|
|
|
|
|
|
instance Show Exceptions where
|
2022-06-08 19:35:44 +03:00
|
|
|
show (Exceptions exns) =
|
2022-02-23 22:32:18 +03:00
|
|
|
unlines
|
2022-06-08 19:35:44 +03:00
|
|
|
[ show i ++ "." ++ exnString
|
|
|
|
| (i, exnString) <- zip [1 :: Int ..] (map indentShow (NE.toList exns))
|
2022-02-23 22:32:18 +03:00
|
|
|
]
|
|
|
|
where
|
|
|
|
indentShow e =
|
|
|
|
case lines (show e) of
|
|
|
|
[] -> ""
|
|
|
|
first : rest ->
|
|
|
|
unlines $ first : map (" " <>) rest
|