ki-1.0.1.2: test/Tests.hs
module Main (main) where
import Control.Concurrent (newEmptyMVar, putMVar, readMVar, takeMVar, threadDelay)
import Control.Concurrent.STM (atomically)
import Control.Exception
import Control.Monad
import Data.IORef
import GHC.IO (unsafeUnmask)
import qualified Ki
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (testCase)
import Prelude
main :: IO ()
main =
defaultMain (testGroup "Unit tests" tests)
tests :: [TestTree]
tests =
[ testCase "`fork` throws ErrorCall when the scope is closed" do
scope <- Ki.scoped pure
(atomically . Ki.await =<< Ki.fork scope (pure ())) `shouldThrow` ErrorCall "ki: scope closed"
pure (),
testCase "`fork` throws ScopeClosing to children when the scope is closing" do
Ki.scoped \scope -> do
_ <-
Ki.forkWith scope Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible} do
-- Naughty: catch and ignore the ScopeClosing delivered to us
result1 <- try @SomeException (threadDelay maxBound)
show result1 `shouldBe` "Left <<internal ki exception: scope closing>>"
-- Try forking a new thread in the closing scope, and assert that (synchronously) throws ScopeClosing
result2 <- try @SomeException (Ki.fork_ scope undefined)
show result2 `shouldBe` "Left <<internal ki exception: scope closing>>"
pure (),
testCase "`awaitAll` succeeds when no threads are alive" do
Ki.scoped (atomically . Ki.awaitAll),
testCase "`fork` propagates exceptions" do
(`shouldThrow` A) do
Ki.scoped \scope -> do
Ki.fork_ scope (throwIO A)
atomically (Ki.awaitAll scope),
testCase "`fork` puts exceptions after propagating" do
(`shouldThrow` A) do
Ki.scoped \scope -> do
mask \restore -> do
thread :: Ki.Thread () <- Ki.fork scope (throwIO A)
restore (atomically (Ki.awaitAll scope)) `catch` \(_ :: SomeException) -> pure ()
atomically (Ki.await thread),
testCase "`fork` forks in unmasked state regardless of parent's masking state" do
Ki.scoped \scope -> do
_ <- Ki.fork scope (getMaskingState `shouldReturn` Unmasked)
_ <- mask_ (Ki.fork scope (getMaskingState `shouldReturn` Unmasked))
_ <- uninterruptibleMask_ (Ki.fork scope (getMaskingState `shouldReturn` Unmasked))
atomically (Ki.awaitAll scope),
testCase "`forkWith` can fork in interruptibly masked state regardless of paren't masking state" do
Ki.scoped \scope -> do
_ <-
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible}
(getMaskingState `shouldReturn` MaskedInterruptible)
_ <-
mask_ do
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible}
(getMaskingState `shouldReturn` MaskedInterruptible)
_ <-
uninterruptibleMask_ do
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible}
(getMaskingState `shouldReturn` MaskedInterruptible)
atomically (Ki.awaitAll scope),
testCase "`forkWith` can fork in uninterruptibly masked state regardless of paren't masking state" do
Ki.scoped \scope -> do
_ <-
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedUninterruptible}
(getMaskingState `shouldReturn` MaskedUninterruptible)
_ <-
mask_ do
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedUninterruptible}
(getMaskingState `shouldReturn` MaskedUninterruptible)
_ <-
uninterruptibleMask_ do
Ki.forkWith
scope
Ki.defaultThreadOptions {Ki.maskingState = MaskedUninterruptible}
(getMaskingState `shouldReturn` MaskedUninterruptible)
atomically (Ki.awaitAll scope),
testCase "`forkTry` can catch sync exceptions" do
Ki.scoped \scope -> do
result :: Ki.Thread (Either A ()) <- Ki.forkTry scope (throw A)
atomically (Ki.await result) `shouldReturn` Left A,
testCase "`forkTry` can propagate sync exceptions" do
(`shouldThrow` A) do
Ki.scoped \scope -> do
thread :: Ki.Thread (Either A2 ()) <- Ki.forkTry scope (throw A)
atomically (Ki.await thread),
testCase "`forkTry` propagates async exceptions" do
(`shouldThrow` B) do
Ki.scoped \scope -> do
thread :: Ki.Thread (Either B ()) <- Ki.forkTry scope (throw B)
atomically (Ki.await thread),
testCase "`forkTry` puts exceptions after propagating" do
(`shouldThrow` A2) do
Ki.scoped \scope -> do
mask \restore -> do
thread :: Ki.Thread (Either A ()) <- Ki.forkTry scope (throwIO A2)
restore (atomically (Ki.awaitAll scope)) `catch` \(_ :: SomeException) -> pure ()
atomically (Ki.await thread),
testCase "child propagates exceptions thrown during cleanup" do
(`shouldThrow` A) do
Ki.scoped \scope -> do
ready <- newEmptyMVar
Ki.forkWith_ scope Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible} do
putMVar ready ()
unsafeUnmask (forever (threadDelay maxBound)) `finally` throwIO A
takeMVar ready,
testCase "regression test https://github.com/awkward-squad/ki/issues/33" do
ref <- newIORef False
ready <- newEmptyMVar
handle (\A -> pure ()) do
Ki.scoped \scope1 -> do
_ <-
Ki.fork scope1 do
readMVar ready
throwIO A
Ki.scoped \scope2 -> do
_ <-
Ki.fork scope2 do
(putMVar ready () >> threadDelay 1_000_000) `catch` \(_ :: SomeException) ->
writeIORef ref True
atomically (Ki.awaitAll scope2)
readIORef ref `shouldReturn` True
]
data A = A
deriving stock (Eq, Show)
deriving anyclass (Exception)
data A2 = A2
deriving stock (Eq, Show)
deriving anyclass (Exception)
data B = B
deriving stock (Eq, Show)
instance Exception B where
toException = asyncExceptionToException
fromException = asyncExceptionFromException
shouldBe :: (Eq a, Show a) => a -> a -> IO ()
shouldBe actual expected = do
unless (actual == expected) (fail ("expected " ++ show expected ++ ", got " ++ show actual))
shouldReturn :: (Eq a, Show a) => IO a -> a -> IO ()
shouldReturn action expected = do
actual <- action
actual `shouldBe` expected
shouldThrow :: (Show a, Eq e, Exception e) => IO a -> e -> IO ()
shouldThrow action expected =
try @SomeException action >>= \case
Left exception | fromException exception == Just expected -> pure ()
Left exception ->
fail ("expected exception " ++ displayException expected ++ ", got exception " ++ displayException exception)
Right value -> fail ("expected exception " ++ displayException expected ++ ", got " ++ show value)