ki-0.1.0: test/unit-tests/Tests.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeApplications #-}
module Main (main) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad (when)
import Data.Functor
import Data.IORef
import Data.Maybe
import qualified Ki.Implicit as Ki
import qualified Ki.Internal
import TestUtils
import Prelude hiding (fail)
main :: IO ()
main = do
test "background context isn't cancelled" do
(isJust <$> Ki.cancelled) `shouldReturn` False
test "new scope doesn't start out cancelled" do
Ki.scoped \_ -> (isJust <$> Ki.cancelled) `shouldReturn` False
test "`cancelScope` observable by scope's `cancelled`" do
Ki.scoped \scope -> do
Ki.cancelScope scope
(isJust <$> Ki.cancelled) `shouldReturn` True
test "`cancelScope` observable by inner scope's `cancelled`" do
Ki.scoped \scope ->
Ki.scoped \_ -> do
Ki.cancelScope scope
(isJust <$> Ki.cancelled) `shouldReturn` True
childtest "`cancelScope` observable by child's `cancelled`" \fork -> do
ref <- newIORef Nothing
Ki.scoped \scope -> do
fork scope do
Ki.cancelScope scope
Ki.cancelled >>= writeIORef ref
Ki.wait scope
(isJust <$> readIORef ref) `shouldReturn` True
childtest "`cancelScope` observable by grandchild's `cancelled`" \fork -> do
ref <- newIORef Nothing
Ki.scoped \scope1 -> do
fork scope1 do
Ki.scoped \scope2 -> do
fork scope2 do
Ki.cancelScope scope1
Ki.cancelled >>= writeIORef ref
Ki.wait scope2
Ki.wait scope1
(isJust <$> readIORef ref) `shouldReturn` True
test "inner scope inherits cancellation" do
Ki.scoped \scope1 -> do
Ki.cancelScope scope1
Ki.scoped \_ -> (isJust <$> Ki.cancelled) `shouldReturn` True
childtest "child thread inherits cancellation" \fork -> do
ref <- newIORef Nothing
Ki.scoped \scope -> do
Ki.cancelScope scope
fork scope (Ki.cancelled >>= writeIORef ref)
Ki.wait scope
(isJust <$> readIORef ref) `shouldReturn` True
childtest "creating a child thread throws ErrorCall when the scope is closed" \fork -> do
scope <- Ki.scoped pure
fork scope (pure ()) `shouldThrow` ErrorCall "ki: scope closed"
test "cancelled child context removes parent's ref to it" do
ctx0 <- atomically Ki.Internal.newCtxSTM
ctx1 <- atomically (Ki.Internal.deriveCtx ctx0)
(length <$> readTVarIO (Ki.Internal.childrenVar ctx0)) `shouldReturn` 1
Ki.Internal.cancelCtx ctx1
(length <$> readTVarIO (Ki.Internal.childrenVar ctx0)) `shouldReturn` 0
test "`wait` succeeds when no threads are alive" do
Ki.scoped Ki.wait
childtest "creates a thread" \fork -> do
parentThreadId <- myThreadId
Ki.scoped \scope -> do
fork scope do
childThreadId <- myThreadId
when (parentThreadId == childThreadId) (fail "didn't create a thread")
Ki.wait scope
forktest "propagates sync exceptions" \fork -> do
shouldThrowSuchThat
( Ki.scoped \scope -> do
fork scope (throwIO A)
Ki.wait scope
)
(\(Ki.ThreadFailed _threadId exception) -> fromException exception == Just A)
forktest "propagates async exceptions" \fork -> do
shouldThrowSuchThat
( Ki.scoped \scope -> do
fork scope (throwIO B)
Ki.wait scope
)
(\(Ki.ThreadFailed _threadId exception) -> fromException exception == Just B)
forktest "doesn't propagate own cancel token exceptions" \fork ->
Ki.scoped \scope -> do
Ki.cancelScope scope
fork scope (atomically Ki.cancelledSTM >>= throwIO)
Ki.wait scope
forktest "propagates ScopeClosing if it isn't ours" \fork ->
shouldThrowSuchThat
( Ki.scoped \scope -> do
fork scope (throwIO Ki.Internal.ScopeClosing)
Ki.wait scope
)
(\(Ki.ThreadFailed _threadId exception) -> fromException exception == Just Ki.Internal.ScopeClosing)
forktest "propagates others' cancel token exceptions" \fork ->
shouldThrowSuchThat
( Ki.scoped \scope -> do
Ki.cancelScope scope
fork scope (throwIO (Ki.Internal.CancelToken 0))
Ki.wait scope
)
(\(Ki.ThreadFailed _threadId exception) -> fromException exception == Just (Ki.Internal.CancelToken 0))
test "`async` returns sync exceptions" do
Ki.scoped \scope -> do
result <- Ki.async @() scope (throw A)
Ki.await result `shouldReturnSuchThat` \case
Left (Ki.ThreadFailed _threadId exception) -> fromException exception == Just A
_ -> False
test "`async` returns async exceptions" do
Ki.scoped \scope -> do
result <- Ki.async @() scope (throw B)
Ki.await result `shouldReturnSuchThat` \case
Left (Ki.ThreadFailed _threadId exception) -> fromException exception == Just B
_ -> False
test "awaiting a failed `fork`ed thread throws the sync exception it failed with" do
Ki.scoped \scope -> do
mask \unmask -> do
thread <- Ki.fork @() scope (throw A)
unmask (Ki.wait scope) `catch` \(Ki.Internal.ThreadFailedAsync _) -> pure ()
Ki.await thread
`shouldThrowSuchThat` \(Ki.ThreadFailed _threadId exception) -> fromException exception == Just A
test "awaiting a failed `fork`ed thread throws the async exception it failed with" do
Ki.scoped \scope -> do
mask \unmask -> do
thread <- Ki.fork @() scope (throw B)
unmask (Ki.wait scope) `catch` \(Ki.Internal.ThreadFailedAsync _) -> pure ()
Ki.await thread
`shouldThrowSuchThat` \(Ki.ThreadFailed _threadId exception) -> fromException exception == Just B
childtest "inherits masking state" \fork -> do
Ki.scoped \scope -> do
fork scope (getMaskingState `shouldReturn` Unmasked)
mask_ (fork scope (getMaskingState `shouldReturn` MaskedInterruptible))
uninterruptibleMask_ (fork scope (getMaskingState `shouldReturn` MaskedUninterruptible))
Ki.wait scope
test "provides an unmasking function (`forkWithUnmask`)" do
Ki.scoped \scope -> do
_thread <- mask_ (Ki.forkWithUnmask scope \unmask -> unmask getMaskingState `shouldReturn` Unmasked)
Ki.wait scope
test "provides an unmasking function (`forkWithUnmask_`)" do
Ki.scoped \scope -> do
mask_ (Ki.forkWithUnmask_ scope \unmask -> unmask getMaskingState `shouldReturn` Unmasked)
Ki.wait scope
test "provides an unmasking function (`asyncWithUnmask`)" do
Ki.scoped \scope -> do
_thread <- (Ki.asyncWithUnmask scope \unmask -> unmask getMaskingState `shouldReturn` Unmasked)
Ki.wait scope
test "thread can be awaited after its scope closes" do
thread <-
Ki.scoped \scope -> do
thread <- Ki.fork scope (pure ())
Ki.wait scope
pure thread
Ki.await thread `shouldReturn` ()
forktest :: String -> (Ki.Context => (Ki.Scope -> (Ki.Context => IO ()) -> IO ()) -> IO ()) -> IO ()
forktest name theTest = do
test (name ++ " (`fork`)") (theTest \scope action -> void (Ki.fork scope action))
test (name ++ " (`fork_`)") (theTest Ki.fork_)
childtest :: String -> (Ki.Context => (Ki.Scope -> (Ki.Context => IO ()) -> IO ()) -> IO ()) -> IO ()
childtest name theTest = do
forktest name theTest
test (name ++ " (`async`)") (theTest \scope action -> void (Ki.async scope action))
data A = A
deriving stock (Eq, Show)
deriving anyclass (Exception)
data B = B
deriving stock (Eq, Show)
instance Exception B where
toException = asyncExceptionToException
fromException = asyncExceptionFromException