packages feed

immortal-0.2.2: tests/test.hs

{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
import Test.Tasty
import Test.Tasty.HUnit
import qualified Control.Immortal as Immortal
import Control.Monad
import Control.Exception
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import System.Timeout

-- Almost bracket, but we don't want to start a thread inside mask
-- See http://ro-che.info/articles/2014-07-30-bracket.html
withImmortal :: IO () -> IO c -> IO c
withImmortal comp inner = do
  thread <- Immortal.create $ const comp
  inner `finally` Immortal.stop thread

withImmortalThread :: (Immortal.Thread -> IO ()) -> (Immortal.Thread -> IO c) -> IO c
withImmortalThread comp inner = do
  thread <- Immortal.create comp
  inner thread `finally` Immortal.stop thread

main :: IO ()
main = defaultMain $ testGroup "Tests"
  [ testCase "is not killed by an exception" $ do
      tv <- atomically $ newTVar True
      immortal <- Immortal.create $ const $ keepTrue tv

      killThread (Immortal.threadId immortal)
      atomically $ writeTVar tv False
      delay
      v <- atomically $ readTVar tv
      assertBool "Thread died" v

  , testCase "never finishes" $ do
      tv <- atomically $ newTVar False
      withImmortal (keepTrue tv) $
        replicateM_ 10 $ do
          atomically $ writeTVar tv False
          delay
          v <- atomically $ readTVar tv
          assertBool "Thread died" v

  , testCase "can be stopped" $ do
      tv <- atomically $ newTVar True
      immortal <- Immortal.create $ const $ keepTrue tv

      Immortal.stop immortal
      atomically $ writeTVar tv False
      delay
      v <- atomically $ readTVar tv
      assertBool "Thread did not stop" (not v)

  , testCase "state is preserved when there are no exceptions" $ do
      tv <- atomically $ newTVar 0
      pid <- flip evalStateT 0 $ Immortal.create $ const $ countToFive tv
      (do
        delay
        v <- atomically $ readTVar tv
        v @?= 5) `finally` Immortal.stop pid

  , testCase "state is reset when there are exceptions" $ do
      tv <- atomically $ newTVar 0
      let
        computation = do
          countToFive tv
          liftIO delay
          error "bah!"
      pid <- flip evalStateT 0 $ Immortal.create $ const computation
      (do
        threadDelay (5*10^5)
        v <- atomically $ readTVar tv
        v @?= 0)
        `finally` Immortal.stop pid

  , testCase "onFinish detects normal exit" $ do
      tv <- atomically $ newTVar Nothing
      let
        comp =
          Immortal.onFinish
            (\r -> atomically $ writeTVar tv (Just r))
            (liftIO delay)
      withImmortal comp $ do
        threadDelay (2*10^5)
        v <- atomically $ readTVar tv
        case v of
          Just (Right ()) -> return ()
          _ -> assertFailure $ "unexpected result: " ++ show v

  , testCase "onFinish detects abnormal exit" $ do
      tv <- atomically $ newTVar Nothing
      let
        comp =
          Immortal.onFinish
            (\r -> atomically $ writeTVar tv (Just r))
            (do liftIO delay; error "bah!")
      withImmortal comp $ do
        threadDelay (2*10^5)
        v <- atomically $ readTVar tv
        case v of
          Just (Left (fromException -> Just (ErrorCall "bah!"))) -> return ()
          _ -> assertFailure $ "unexpected result: " ++ show v

  , testCase "onUnexpectedFinish fires when thread is immortal" $ do
      tv <- atomically $ newTVar Nothing
      let
        comp thread =
          Immortal.onUnexpectedFinish thread
            (\r -> atomically $ writeTVar tv (Just r))
            (liftIO delay)
      withImmortalThread comp $ \_ -> do
        threadDelay (2*10^5)
        v <- atomically $ readTVar tv
        case v of
          Just (Right ()) -> return ()
          _ -> assertFailure $ "unexpected result: " ++ show v

  , testCase "onUnexpectedFinish does not fire when thread is mortal" $ do
      tv <- atomically $ newTVar Nothing
      let
        comp thread =
          Immortal.onUnexpectedFinish thread
            (\r -> atomically $ writeTVar tv (Just r))
            (do Immortal.mortalize thread; liftIO delay)
      withImmortalThread comp $ \_ -> do
        threadDelay (2*10^5)
        v <- atomically $ readTVar tv
        case v of
          Nothing -> return ()
          _ -> assertFailure $ "unexpected result: " ++ show v

  , testCase "mortalize allows thread to finish" $ do
      tv <- atomically $ newTVar True
      t <- Immortal.create $ const $ keepTrue tv
      Immortal.mortalize t
      atomically $ writeTVar tv False
      delay
      v1 <- atomically $ readTVar tv
      -- thread was waiting for this; v1 should be True
      v1 @?= True
      -- since the thread was mortalized, it shouldn't be restarted
      -- so try the same actions again
      atomically $ writeTVar tv False
      delay
      v2 <- atomically $ readTVar tv
      -- and we now should get False
      v2 @?= False

  , testCase "immortalize cancels mortalize" $ do
      -- this is the copy of the previous test, only after mortalize we
      -- immediately call immortalize
      tv <- atomically $ newTVar True
      t <- Immortal.create $ const $ keepTrue tv
      Immortal.mortalize t
      Immortal.immortalize t
      atomically $ writeTVar tv False
      delay
      v1 <- atomically $ readTVar tv
      v1 @?= True
      atomically $ writeTVar tv False
      delay
      v2 <- atomically $ readTVar tv
      v2 @?= True
      Immortal.stop t

  , testCase "cancelling from within the thread works" $ do
      -- tv1 checks that the thread stopped running
      -- tv2 checks that the exception was thrown
      tv1 <- atomically $ newTVar False
      tv2 <- atomically $ newTVar False
      _ <- Immortal.create $ \thread -> do
        keepTrue tv1
        Immortal.stop thread
        atomically $ writeTVar tv1 True

      delay
      atomically $ writeTVar tv1 False
      delay
      v1 <- atomically $ readTVar tv1
      v2 <- atomically $ readTVar tv2
      v1 @?= False
      v2 @?= False

  , testCase "wait is called after the thread is stopped" $ do
      thread <- Immortal.create $ \_ -> threadDelay maxBound
      _ <- forkIO $ threadDelay (10^4) >> Immortal.stop thread
      result <- timeout (10^5) $ Immortal.wait thread

      result @?= Just ()
  ]

keepTrue :: TVar Bool -> IO ()
keepTrue tv = atomically $ do
  v <- readTVar tv
  check $ not v
  writeTVar tv True

sleep :: IO ()
sleep = threadDelay (60 * 10^6) -- 1 min

delay :: IO ()
delay = threadDelay (10^5) -- 0.1 s

countToFive :: TVar Int -> StateT Int IO ()
countToFive tv = do
  n <- get
  liftIO $ atomically $ writeTVar tv n
  if n == 5
    then liftIO sleep
    else put $! n+1