packages feed

hedgehog-extras-0.6.3.0: test/Hedgehog/Extras/Test/TestWatchdogSpec.hs

{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE LambdaCase #-}

module Hedgehog.Extras.Test.TestWatchdogSpec where

import           Control.Concurrent
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Except (runExceptT)
import           Control.Monad.Trans.Resource (ResourceT, runResourceT)
import           Control.Monad.Trans.Writer.Lazy (runWriterT)
import           Data.Function
import           Data.List (isPrefixOf)
import           Data.Time.Clock as D
import           GHC.Conc
import           GHC.Stack
import           Hedgehog (Property, (===))
import qualified Hedgehog as H
import qualified Hedgehog.Extras as H
import qualified Hedgehog.Extras.Stock as H
import           Hedgehog.Extras.Test.TestWatchdog
import qualified Hedgehog.Internal.Property as H
import           Prelude
import qualified System.Process as P

-- | Check that watchdog kills test case which waits without an end
hprop_check_watchdog_kills_hanged_thread :: Property
hprop_check_watchdog_kills_hanged_thread = H.propertyOnce $ do
  let watchdogCfg = WatchdogConfig 1
  childTid <- H.newEmptyMVar
  tripwire <- H.makeTripwire

  (result, _) <- spawnTestT $ runWithWatchdog_ watchdogCfg $ do
    liftIO $ myThreadId >>= H.putMVar childTid
    -- simulate thread hang
    void $ H.threadDelay 3_000_000
    H.trip tripwire

  assertWatchdogExceptionWasRaised result

  -- make sure that we  didn't trigger the tripwire
  H.assertNotTripped tripwire
  childStatus <- liftIO $ H.readMVar childTid >>= threadStatus
  childStatus === ThreadFinished

-- | Check that watchdog kills test case which spawns:
--  - a process waiting forever
--  - a child threads waiting
hprop_check_watchdog_kills_hanged_thread_with_its_children :: Property
hprop_check_watchdog_kills_hanged_thread_with_its_children = H.propertyOnce $ do
  let watchdogCfg = WatchdogConfig 1
  childTid <- H.newEmptyMVar
  grandChildTid1 <- H.newEmptyMVar
  grandChildTid2 <- H.newEmptyMVar
  procHandle <- H.newEmptyMVar
  childTripwire <- H.makeTripwire
  grandChildTripwire1 <- H.makeTripwire
  grandChildTripwire2 <- H.makeTripwire

  (result, _) <- spawnTestT $ runWithWatchdog_ watchdogCfg $ do
    liftIO $ myThreadId >>= H.putMVar childTid

    unless H.isWin32 $ do
      (_, _, _, h, _) <- H.createProcess $ P.shell "tail -f /dev/null"
      H.putMVar procHandle h

    H.asyncRegister_ $ do
      liftIO $ myThreadId >>= H.putMVar grandChildTid1
      threadDelay 3_000_000
      H.trip_ grandChildTripwire1

    H.asyncRegister_ $ do
      liftIO $ myThreadId >>= H.putMVar grandChildTid2
      threadDelay 3_000_000
      H.trip_ grandChildTripwire2

    void $ H.threadDelay 3_000_000
    H.trip childTripwire

  -- make sure that we didn't trigger the tripwire
  H.assertNotTripped childTripwire
  H.assertNotTripped grandChildTripwire1
  H.assertNotTripped grandChildTripwire2

  assertWatchdogExceptionWasRaised result

  -- Give OS 5 seconds to do the process cleanup
  deadline <- D.addUTCTime 5 <$> liftIO D.getCurrentTime

  H.byDeadlineM 0.2 deadline "childStatus" $ do
    childStatus <- liftIO $ H.readMVar childTid >>= threadStatus
    childStatus === ThreadFinished

  H.byDeadlineM 0.2 deadline "grandChildStatus1" $ do
    grandChildStatus1 <- liftIO $ H.readMVar grandChildTid1 >>= threadStatus
    grandChildStatus1 === ThreadFinished

  H.byDeadlineM 0.2 deadline "grandChildStatus2" $ do
    grandChildStatus2 <- liftIO $ H.readMVar grandChildTid2 >>= threadStatus
    grandChildStatus2 === ThreadFinished

  -- check that tail process got killed
  unless H.isWin32 $
    H.byDeadlineM 0.2 deadline "tailPid" $ do
      tailPid <- liftIO $ H.readMVar procHandle >>= P.getPid
      tailPid === Nothing

hprop_check_asyncRegister_finishes_with_test :: Property
hprop_check_asyncRegister_finishes_with_test = H.propertyOnce $ do
  childTid <- H.newEmptyMVar
  grandChildTid1 <- H.newEmptyMVar
  grandChildTid2 <- H.newEmptyMVar
  childTripwire <- H.makeTripwire
  grandChildTripwire1 <- H.makeTripwire
  grandChildTripwire2 <- H.makeTripwire

  -- test that asyncRegister_ gets killed when the main thread finishes
  (result, _) <- spawnTestT $ do
    liftIO $ myThreadId >>= H.putMVar childTid

    H.asyncRegister_ $ do
      liftIO $ myThreadId >>= H.putMVar grandChildTid1
      threadDelay 10_000_000
      H.trip_ grandChildTripwire1

    H.asyncRegister_ $ do
      liftIO $ myThreadId >>= H.putMVar grandChildTid2
      threadDelay 10_000_000
      H.trip_ grandChildTripwire2

    H.trip_ childTripwire

  result === Right ()
  -- double check that main thread finished successfully
  H.assertTripped childTripwire
  H.assertNotTripped grandChildTripwire1
  H.assertNotTripped grandChildTripwire2

  -- Give OS 5 seconds to do the process cleanup
  deadline <- D.addUTCTime 5 <$> liftIO D.getCurrentTime

  H.byDeadlineM 0.2 deadline "childStatus" $ do
    childStatus <- liftIO $ H.readMVar childTid >>= threadStatus
    childStatus === ThreadFinished

  H.byDeadlineM 0.2 deadline "grandChildStatus1" $ do
    grandChildStatus1 <- liftIO $ H.readMVar grandChildTid1 >>= threadStatus
    grandChildStatus1 === ThreadFinished

  H.byDeadlineM 0.2 deadline "grandChildStatus2" $ do
    grandChildStatus2 <- liftIO $ H.readMVar grandChildTid2 >>= threadStatus
    grandChildStatus2 === ThreadFinished


assertWatchdogExceptionWasRaised :: HasCallStack
                                 => H.MonadTest m
                                 => MonadFail m
                                 => Either H.Failure a
                                 -> m ()
assertWatchdogExceptionWasRaised = withFrozenCallStack $ \case
  Right _ -> do
    H.note_ "Expected failure instead of Right"
    H.failure
  Left (H.Failure _ msg _) -> do
    -- check we've failed because of watchdog
    _header:exception:_ <- pure $ lines msg
    H.note_ $ "Received exception:"
    H.assertWith exception $
      isPrefixOf "WatchdogException: "


-- | Spawn TestT in an async. Waits for the async and logs the result as well as errors journal on failure
spawnTestT :: HasCallStack
         => H.MonadTest m
         => MonadIO m
         => Show a
         => H.TestT (ResourceT IO) a
         -> m (Either H.Failure a, H.Journal)
spawnTestT testt = withFrozenCallStack $ do
  (res, log') <- H.evalIO $
    H.withAsync
      (runResourceT . runWriterT . runExceptT $ H.unTest testt)
      H.wait
  H.noteShow_ res
  H.noteShow_ log'
  pure (res, log')