packages feed

slave-thread-1.0.3: test/Main.hs

module Main where

import Prelude
import Test.QuickCheck.Instances
import Test.Tasty
import Test.Tasty.Runners
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import qualified Test.QuickCheck as QuickCheck
import qualified Test.QuickCheck.Property as QuickCheck
import qualified SlaveThread as S
import qualified Control.Concurrent.SSem as SSem


main =
  defaultMain $
  testGroup "All" $ [
    testCase "Failing in finalizer doesn't break everything" $ do
      finalizer1CalledVar <- newTVarIO False
      finalizer2CalledVar <- newTVarIO False
      result <- let
        finalizer1 =
          atomically $ writeTVar finalizer1CalledVar True
        finalizer2 =
          do
            atomically $ writeTVar finalizer2CalledVar True
            throwIO (userError "finalizer2 failed")
        in try @SomeException $ do
          S.forkFinally finalizer1 $ do
            S.forkFinally finalizer2 $ threadDelay 100
          threadDelay (10^4)
      finalizer2Called <- atomically (readTVar finalizer2CalledVar)
      finalizer1Called <- atomically (readTVar finalizer1CalledVar)
      assertEqual "finalizer2 not called" True finalizer2Called
      assertEqual "finalizer1 not called" True finalizer1Called
      assertEqual "Invalid result" "Left user error (finalizer2 failed)" (show result)
    ,
    testCase "Forked threads run fine" $ do
      replicateM_ 100000 $ do
        var <- newMVar 0
        let increment = modifyMVar_ var (return . succ)
        semaphore <- SSem.new 0
        S.fork $ do
          increment
          semaphore' <- SSem.new (-1)
          S.fork $ do
            increment
            SSem.signal semaphore'
          S.fork $ do
            increment
            SSem.signal semaphore'
          SSem.wait semaphore'
          SSem.signal semaphore
        SSem.wait semaphore
        assertEqual "" 3 =<< readMVar var
    ,
    testCase "Killing a thread kills deep slaves" $ do
      replicateM_ 100000 $ do
        var <- newMVar 0
        semaphore <- SSem.new 0
        thread <-
          S.forkFinally (SSem.signal semaphore) $ do
            join $ forkWait $ do
              join $ forkWait $ do
                w <- forkWait $ do
                  threadDelay $ 10^6
                  modifyMVar_ var (return . succ)
                threadDelay $ 10^6
                modifyMVar_ var (return . succ)
                w
        killThread thread
        SSem.wait semaphore
        assertEqual "" 0 =<< readMVar var
    ,
    testCase "Dying normally kills slaves" $ do
      replicateM_ 100000 $ do
        var <- newIORef 0
        let increment = modifyIORef var (+1)
        semaphore <- SSem.new 0
        S.forkFinally (SSem.signal semaphore) $ do
          S.fork $ do
            threadDelay $ 10^6
            increment
          S.fork $ do
            threadDelay $ 10^6
            increment
        SSem.wait semaphore
        assertEqual "" 0 =<< readIORef var
    ,
    testCase "Finalization is in order" $ do
      replicateM_ 100000 $ do
        var <- newMVar []
        semaphore <- SSem.new 0
        S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (1:)) >> SSem.signal semaphore)) $ do
          semaphore' <- SSem.new 0
          S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (2:)) >> SSem.signal semaphore')) $ do
            S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3:)))) $ return ()
            S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3:)))) $ return ()
          SSem.wait semaphore'
        SSem.wait semaphore
        assertEqual "" [1,2,3,3] =<< readMVar var
    ,
    testCase "Exceptions don't get lost" $ do
      replicateM_ 100000 $ do
        result <- try @SomeException $ do
          S.fork $ do
            S.fork $ do
              error "!"
            threadDelay $ 10^6
          threadDelay $ 10^6
        assertBool "" (isLeft result)
    ,
    testCase "Slaves are finalized before master" $ do
      replicateM_ 100000 $ do
        ready <- newEmptyMVar
        var <- newEmptyTMVarIO
        thread <-
          S.forkFinally (atomically (tryPutTMVar var 1)) $ do
            S.forkFinally (atomically (tryPutTMVar var 0)) $
              threadDelay $ 10^6
            putMVar ready ()
            threadDelay $ 10^6
        takeMVar ready
        killThread thread
        assertEqual "First finalizer is not slave" 0 =<< atomically (readTMVar var)
    ,
    testCase "Slave thread finalizer is not interrupted by its own death (#11)" $ do
      -- Set up the ref that should be written to by thread 2's finalizer,
      -- otherwise there's a bug.
      ref <- newIORef True

      -- Let the main thread know when it should check the above IORef.
      done <- newEmptyMVar

      -- The gist of the test below: assert that, when a thread fails (here, the
      -- inner thread), its finalizer is not interrupted by a ThreadKilled
      -- thrown by the parent, which was originally triggered by its own death.

      S.forkFinally (putMVar done ()) $ do

        -- Let thread 2 know when it should die, with thread 1's exception
        -- handler in place.
        ready <- newEmptyMVar

        S.forkFinally
          (catch @SomeException (threadDelay (10^5)) (\_ -> writeIORef ref False)) $ do

          -- Wait until thread 1 is ready for us to die.
          takeMVar ready

          -- Die.
          throwIO (userError "")

        catch @SomeException
          ( -- Tell thread 2 we're ready for it to die
            putMVar ready () >>

            -- Sleep until thread 2 kills us.
            threadDelay (10^5*2)
          )
          -- Ignore thread 2's exception, so we don't propagate it up to the
          -- main thread.
          (\ _ -> return ())

      takeMVar done
      assertBool "Slave thread finalizer interrupted" =<< readIORef ref
    ,
    testCase "Master kills all slaves, even if it is thrown an exception during (#13)" $ do
      survived <- newEmptyTMVarIO
      ready <- newEmptyMVar
      done <- newEmptyMVar
      thread <-
        S.fork $ do
          S.forkFinally (atomically (tryPutTMVar survived True)) $ do
            uninterruptibleMask_ (putMVar ready () >> threadDelay (10^6))
            atomically (putTMVar survived False)
          takeMVar ready
          putMVar done ()
      takeMVar done
      threadDelay $ 10^5 -- be reasonably sure it's trying to kill its child
      killThread thread
      assertBool "Slave thread not killed by master" =<< atomically (takeTMVar survived)
  ]

forkWait :: IO a -> IO (IO ())
forkWait io =
  do
    v <- newEmptyMVar
    S.fork $ do
      r <- try @SomeException io
      putMVar v ()
      either throwIO return r
    return $ takeMVar v