|
1 | 1 | {-# LANGUAGE NumericUnderscores #-}
|
2 | 2 | module Hedgehog.Extras.Test.TestWatchdogSpec where
|
3 | 3 |
|
| 4 | +import Control.Concurrent |
| 5 | +import Control.Monad |
| 6 | +import Control.Monad.IO.Class |
| 7 | +import Control.Monad.Trans.Except (runExceptT) |
| 8 | +import Control.Monad.Trans.Resource (ResourceT, runResourceT) |
| 9 | +import Control.Monad.Trans.Writer.Lazy (runWriterT) |
4 | 10 | import Data.Function
|
5 |
| -import Data.Semigroup |
6 |
| -import Hedgehog (Property) |
| 11 | +import Data.List (isPrefixOf) |
| 12 | +import Data.Time.Clock as D |
| 13 | +import GHC.Conc |
| 14 | +import GHC.Stack |
| 15 | +import Hedgehog (Property, (===)) |
7 | 16 | import qualified Hedgehog as H
|
8 | 17 | import qualified Hedgehog.Extras as H
|
9 |
| -import qualified Hedgehog.Extras.Stock.IO.Network.Port as IO |
10 |
| -import Hedgehog.Extras.Test.TestWatchdog |
11 |
| -import qualified Hedgehog.Extras.Test.Concurrent as H |
| 18 | +import qualified Hedgehog.Extras.Stock as H |
| 19 | +import Hedgehog.Extras.Test.TestWatchdog |
12 | 20 | import qualified Hedgehog.Internal.Property as H
|
13 |
| -import Control.Monad.Trans.Writer.Lazy (runWriterT) |
14 |
| -import Control.Monad.Trans.Except (runExceptT) |
15 |
| -import qualified Network.Socket as N |
16 |
| -import Text.Show |
17 |
| -import Control.Monad |
18 |
| -import Prelude |
19 |
| -import Control.Monad.IO.Class |
20 |
| -import GHC.Stack |
21 |
| -import Control.Concurrent |
22 |
| -import GHC.Conc |
| 21 | +import Prelude |
| 22 | +import qualified System.Process as P |
23 | 23 |
|
| 24 | +-- | Check that watchdog kills test case which waits without an end |
24 | 25 | hprop_check_watchdog_kills_hanged_thread :: Property
|
25 | 26 | hprop_check_watchdog_kills_hanged_thread = H.propertyOnce $ do
|
26 | 27 | let watchdogCfg = WatchdogConfig 1
|
27 |
| - childTid <- liftIO $ newEmptyMVar |
28 |
| - (res, log') <- runTestT $ runWithWatchdog_ watchdogCfg $ do |
29 |
| - liftIO $ myThreadId >>= putMVar childTid |
30 |
| - void . forever $ H.threadDelay 1_000_000 |
31 |
| - H.success |
32 |
| - H.threadDelay 1_000_000 |
33 |
| - H.noteShowM_ . liftIO $ (readMVar childTid >>= threadStatus) |
| 28 | + childTid <- H.newEmptyMVar |
| 29 | + tripwire <- H.makeTripwire |
| 30 | + |
| 31 | + (result, _) <- spawnTestT $ runWithWatchdog_ watchdogCfg $ do |
| 32 | + liftIO $ myThreadId >>= H.putMVar childTid |
| 33 | + -- simulate thread hang |
| 34 | + void $ H.threadDelay 3_000_000 |
| 35 | + H.triggerTripwire tripwire |
| 36 | + |
| 37 | + _ <- case result of |
| 38 | + Right () -> do |
| 39 | + H.note_ "Expected failure instead of Right" |
| 40 | + H.failure |
| 41 | + Left (H.Failure _ msg _) -> do |
| 42 | + -- check we've failed because of watchdog |
| 43 | + _header:exception:_ <- pure $ lines msg |
| 44 | + H.assertWith exception $ |
| 45 | + isPrefixOf "WatchdogException: " |
| 46 | + |
| 47 | + -- make sure that we didn't trigger the tripwire |
| 48 | + H.checkTripwire tripwire |
| 49 | + childStatus <- liftIO $ H.readMVar childTid >>= threadStatus |
| 50 | + childStatus === ThreadFinished |
| 51 | + |
| 52 | +-- | Check that watchdog kills test case which spawns: |
| 53 | +-- - a process waiting forever |
| 54 | +-- - a child threads waiting |
| 55 | +hprop_check_watchdog_kills_hanged_thread_with_its_children :: Property |
| 56 | +hprop_check_watchdog_kills_hanged_thread_with_its_children = H.propertyOnce $ do |
| 57 | + let watchdogCfg = WatchdogConfig 1 |
| 58 | + childTid <- H.newEmptyMVar |
| 59 | + grandChildTid1 <- H.newEmptyMVar |
| 60 | + grandChildTid2 <- H.newEmptyMVar |
| 61 | + procHandle <- H.newEmptyMVar |
| 62 | + childTripwire <- H.makeTripwire |
| 63 | + grandChildTripwire1 <- H.makeTripwire |
| 64 | + grandChildTripwire2 <- H.makeTripwire |
| 65 | + |
| 66 | + (result, _) <- spawnTestT $ runWithWatchdog_ watchdogCfg $ do |
| 67 | + liftIO $ myThreadId >>= H.putMVar childTid |
| 68 | + |
| 69 | + unless H.isWin32 $ do |
| 70 | + (_, _, _, h, _) <- H.createProcess $ P.shell "tail -f /dev/null" |
| 71 | + H.putMVar procHandle h |
| 72 | + |
| 73 | + H.asyncRegister_ $ do |
| 74 | + liftIO $ myThreadId >>= H.putMVar grandChildTid1 |
| 75 | + threadDelay 3_000_000 |
| 76 | + H.triggerTripwire grandChildTripwire1 |
| 77 | + |
| 78 | + H.asyncRegister_ $ do |
| 79 | + liftIO $ myThreadId >>= H.putMVar grandChildTid2 |
| 80 | + threadDelay 3_000_000 |
| 81 | + H.triggerTripwire grandChildTripwire2 |
| 82 | + |
| 83 | + void $ H.threadDelay 3_000_000 |
| 84 | + H.triggerTripwire childTripwire |
| 85 | + |
| 86 | + _ <- case result of |
| 87 | + Right () -> do |
| 88 | + H.note_ "Expected failure instead of Right" |
| 89 | + H.failure |
| 90 | + Left (H.Failure _ msg _) -> do |
| 91 | + -- check we've failed because of watchdog |
| 92 | + _header:exception:_ <- pure $ lines msg |
| 93 | + H.assertWith exception $ |
| 94 | + isPrefixOf "WatchdogException: " |
| 95 | + |
| 96 | + -- Give OS 5 seconds to do the process cleanup |
| 97 | + deadline <- D.addUTCTime 5 <$> liftIO D.getCurrentTime |
| 98 | + -- make sure that we didn't trigger the tripwire |
| 99 | + H.checkTripwire childTripwire |
| 100 | + H.checkTripwire grandChildTripwire1 |
| 101 | + H.checkTripwire grandChildTripwire2 |
| 102 | + |
| 103 | + H.byDeadlineM 0.2 deadline "childStatus" $ do |
| 104 | + childStatus <- liftIO $ H.readMVar childTid >>= threadStatus |
| 105 | + childStatus === ThreadFinished |
| 106 | + |
| 107 | + H.byDeadlineM 0.2 deadline "grandChildStatus1" $ do |
| 108 | + grandChildStatus1 <- liftIO $ H.readMVar grandChildTid1 >>= threadStatus |
| 109 | + grandChildStatus1 === ThreadFinished |
| 110 | + |
| 111 | + H.byDeadlineM 0.2 deadline "grandChildStatus1" $ do |
| 112 | + grandChildStatus2 <- liftIO $ H.readMVar grandChildTid1 >>= threadStatus |
| 113 | + grandChildStatus2 === ThreadFinished |
| 114 | + |
| 115 | + -- check that tail process got killed |
| 116 | + unless H.isWin32 $ |
| 117 | + H.byDeadlineM 0.2 deadline "tailPid" $ do |
| 118 | + tailPid <- liftIO $ H.readMVar procHandle >>= P.getPid |
| 119 | + tailPid === Nothing |
| 120 | + |
34 | 121 | H.failure
|
35 | 122 |
|
36 |
| -runTestT :: HasCallStack |
| 123 | +-- | Spawn TestT in an async. Waits for the async and logs the result as well as errors journal on failure |
| 124 | +spawnTestT :: HasCallStack |
37 | 125 | => H.MonadTest m
|
38 | 126 | => MonadIO m
|
39 | 127 | => Show a
|
40 |
| - => H.TestT IO a |
| 128 | + => H.TestT (ResourceT IO) a |
41 | 129 | -> m (Either H.Failure a, H.Journal)
|
42 |
| -runTestT testt = withFrozenCallStack $ do |
43 |
| - (res, log') <- H.evalIO . runWriterT . runExceptT . H.unTest $ testt |
| 130 | +spawnTestT testt = withFrozenCallStack $ do |
| 131 | + (res, log') <- H.evalIO $ |
| 132 | + H.withAsync |
| 133 | + (runResourceT . runWriterT . runExceptT $ H.unTest testt) |
| 134 | + H.wait |
44 | 135 | H.noteShow_ res
|
45 | 136 | H.noteShow_ log'
|
46 | 137 | pure (res, log')
|
|
0 commit comments