packages feed

skeletest-0.4.0: src/Skeletest/Internal/Hooks.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE UndecidableInstances #-}

module Skeletest.Internal.Hooks (
  Hooks (..),
  defaultHooks,

  -- * Runtime
  UserHooks,
  setUserHooks,
  userHooks,

  -- * Hook implementation
  Hook (..),
  HookDef (..),
  HookPriority (..),
  runHook,

  -- * Hook DSL
  runEarly,
  runLate,
  mkHook,
  mkHook_,
  mkPreHook,
  mkPreHook_,
  mkPostHook,
  mkPostHook_,

  -- * Specific hooks

  -- ** modifySpecRegistry
  ModifySpecRegistryHook,
  ModifySpecRegistryHookContext (..),

  -- ** runTest
  RunTestHook,
  RunTestHookContext (..),

  -- ** onTestFailure
  OnTestFailureHook,
  OnTestFailureHookContext (..),

  -- ** runSpecs
  RunSpecsHook,
  RunSpecsHookContext (..),

  -- ** modifyTestSummary
  ModifyTestSummaryHook,
  ModifyTestSummaryHookContext (..),
) where

import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Text (Text)
import GHC.Records (HasField (..))
import Skeletest.Internal.Exit (TestExitCode)
import Skeletest.Internal.Hooks.HookDef
import Skeletest.Internal.Spec.Tree (SpecRegistry)
import Skeletest.Internal.TestInfo (TestInfo)
import Skeletest.Internal.TestRunner (TestResult)
import Skeletest.Internal.TestTargets (TestTargets)
import System.IO.Unsafe (unsafePerformIO)
import UnliftIO.Exception (SomeException)

-- | Hooks for extending Skeletest.
--
-- Use 'defaultHooks' instead of using v'Hooks' directly, to minimize
-- breaking changes.
data Hooks = Hooks
  { modifySpecRegistry :: ModifySpecRegistryHook
  , runTest :: RunTestHook
  , onTestFailure :: OnTestFailureHook
  , runSpecs :: RunSpecsHook
  , modifyTestSummary :: ModifyTestSummaryHook
  }

instance Semigroup Hooks where
  hooks1 <> hooks2 =
    Hooks
      { modifySpecRegistry = hooks1.modifySpecRegistry <> hooks2.modifySpecRegistry
      , runTest = hooks1.runTest <> hooks2.runTest
      , onTestFailure = hooks1.onTestFailure <> hooks2.onTestFailure
      , runSpecs = hooks1.runSpecs <> hooks2.runSpecs
      , modifyTestSummary = hooks1.modifyTestSummary <> hooks2.modifyTestSummary
      }
instance Monoid Hooks where
  mempty = defaultHooks

defaultHooks :: Hooks
defaultHooks =
  Hooks
    { modifySpecRegistry = mempty
    , runTest = mempty
    , onTestFailure = mempty
    , runSpecs = mempty
    , modifyTestSummary = mempty
    }

{----- Runtime -----}

userHooksRef :: IORef Hooks
userHooksRef = unsafePerformIO $ newIORef defaultHooks

setUserHooks :: Hooks -> IO ()
setUserHooks = writeIORef userHooksRef

userHooks :: UserHooks
userHooks = unsafePerformIO $ UserHooks <$> readIORef userHooksRef

newtype UserHooks = UserHooks Hooks

instance
  (HasField field Hooks (Hook ctx inp out)) =>
  HasField field UserHooks (ctx -> inp -> (inp -> IO out) -> IO out)
  where
  getField (UserHooks hooks) = runHook (getField @field hooks)

{----- modifySpecRegistry -----}

-- | Modify the specs in the test suite.
type ModifySpecRegistryHook =
  Hook
    ModifySpecRegistryHookContext
    SpecRegistry
    SpecRegistry

data ModifySpecRegistryHookContext = ModifySpecRegistryHookContext
  { testTargets :: TestTargets
  }

{----- runTest -----}

-- | Modify how a test is executed
type RunTestHook =
  Hook
    RunTestHookContext
    ()
    TestResult

data RunTestHookContext = RunTestHookContext
  { testInfo :: TestInfo
  }

{----- onTestFailure -----}

-- | Modify what happens if a test fails.
type OnTestFailureHook =
  Hook
    OnTestFailureHookContext
    SomeException
    TestResult

data OnTestFailureHookContext = OnTestFailureHookContext
  { testInfo :: TestInfo
  }

{----- runSpecs -----}

-- | Modify the action to run specs.
type RunSpecsHook =
  Hook
    RunSpecsHookContext
    SpecRegistry
    TestExitCode

data RunSpecsHookContext = RunSpecsHookContext
  {
  }

{----- modifyTestSummary -----}

-- | Modify the test summary at the end of the report.
type ModifyTestSummaryHook =
  Hook
    ModifyTestSummaryHookContext
    Text
    Text

data ModifyTestSummaryHookContext = ModifyTestSummaryHookContext
  {
  }