packages feed

hedgehog-extras-0.6.3.0: src/Hedgehog/Extras/Test/Tripwire.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}

-- | This modules provides a tripwire abstraction. You can use tripwire as a detection mechanism if the code
-- path was executed. Trip a tripwire with 'trip' in the place where you'd like to detect if it was
-- reached. The tripwire can then be checked in the other place in the code using for example 'isTripped' or
-- 'assertNotTripped'.
module Hedgehog.Extras.Test.Tripwire
  (
  -- * Create a tripwire
    Tripwire
  , makeTripwire
  , makeTripwireWithLabel
  -- * Tripwire operations
  , trip
  , trip_
  , isTripped
  , getTripSite
  , resetTripwire
  -- * Assertions
  , assertNotTripped
  , assertTripped
  ) where

import           Control.Monad.IO.Class
import           GHC.Stack

import           Control.Concurrent.MVar
import           Control.Monad
import           Data.IORef
import           Data.Maybe
import           Hedgehog (MonadTest)
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Internal.Property as H
import           Prelude
import           System.IO.Unsafe (unsafePerformIO)

-- | Counter used to allocate consecutive IDs to tripwires
tripwireCounter :: IORef Int
tripwireCounter = unsafePerformIO $ newIORef 0
{-# NOINLINE tripwireCounter #-}

-- | Represents a tripwire which can be tripped only once. It can be used to detect if a particular code path
-- was reached.
data Tripwire = Tripwire
  { tripwireId :: !String -- ^ a label for identifying the tripwire
  , tripSite :: MVar CallStack -- ^ call stack of the trip site
  }

instance Show Tripwire where
  show Tripwire{tripwireId} = "Tripwire " <> tripwireId

-- | Creates a new tripwire
makeTripwire :: MonadIO m => m Tripwire
makeTripwire = liftIO $ do
  id' <- atomicModifyIORef' tripwireCounter (join (,) . (+1))
  Tripwire (show id') <$> newEmptyMVar

-- | Creates a new tripwire with a label, which is visible when 'show'ed: @Tripwire mylabel@
makeTripwireWithLabel :: MonadIO m
                      => String
                      -> m Tripwire
makeTripwireWithLabel label = liftIO $ do
  Tripwire label <$> newEmptyMVar

-- | Triggers the tripwire and registers the place of the first trigger. Idempotent.
-- Prints the information in the test log about tripping the tripwire.
trip :: HasCallStack
     => MonadIO m
     => MonadTest m
     => Tripwire
     -> m ()
trip t@Tripwire{tripSite} = withFrozenCallStack $ do
  H.note_ $ show t <> " has been tripped"
  void . liftIO $ tryPutMVar tripSite callStack

-- | Triggers the tripwire and registers the place of the first trigger. Idempotent. A silent variant of
-- 'trip' which does not require 'MonadTest', but also does not log the information about tripping.
trip_ :: HasCallStack
      => MonadIO m
      => Tripwire
      -> m ()
trip_ Tripwire{tripSite} = withFrozenCallStack $ do
  void . liftIO $ tryPutMVar tripSite callStack

-- | Restore tripwire to initial non triggered state
resetTripwire :: MonadIO m
              => Tripwire
              -> m ()
resetTripwire Tripwire{tripSite} = liftIO $ void $ tryTakeMVar tripSite

-- | Return the call stack, where the tripwire was tripped - if it was tripped.
getTripSite :: MonadIO m
            => Tripwire
            -> m (Maybe CallStack)
getTripSite Tripwire{tripSite} = liftIO $ tryReadMVar tripSite

-- | Check if the tripwire was tripped.
isTripped :: MonadIO m
          => Tripwire
          -> m Bool
isTripped Tripwire{tripSite} = liftIO $ not <$> isEmptyMVar tripSite

-- | Fails the test if the tripwire was triggered. Prints the call stack where the tripwire was triggered.
assertNotTripped :: HasCallStack
                 => MonadTest m
                 => MonadIO m
                 => Tripwire
                 -> m ()
assertNotTripped tripwire = withFrozenCallStack $ do
  mTripSite <- getTripSite tripwire
  forM_ mTripSite $ \cs -> do
    H.note_ $ show tripwire <> " has been tripped at: " <> prettyCallStack cs
    H.failure

-- | Fails the test if the tripwire was not triggered yet.
assertTripped :: HasCallStack
              => MonadTest m
              => MonadIO m
              => Tripwire
              -> m ()
assertTripped tripwire = withFrozenCallStack $ do
  mTripSite <- getTripSite tripwire
  when (isNothing mTripSite) $ do
    H.note_ $ show tripwire <> " was not tripped"
    H.failure