linear-locks-0.1.0.1: test/Test/LinearLocks/MutexSpec.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE NoFieldSelectors #-}
module Test.LinearLocks.MutexSpec where
import Control.Concurrent (ThreadId, myThreadId)
import Control.Concurrent.MVar qualified as MVar
import Control.Exception (SomeException, throwIO, try)
import Control.Functor.Linear qualified as L
import Control.Monad (void)
import Control.Monad.IO.Class.Linear qualified as L
import Data.Function ((&))
import GHC.Conc (atomically)
import LinearLocks
import LinearLocks.Internal qualified as Internal
import LinearLocks.Internal.Mutex qualified as Internal
import LinearLocks.Mutex qualified as Mutex
import ListT qualified
import Prelude.Linear (Ur (..))
import Prelude.Linear qualified as L hiding (IO)
import StmContainers.Set qualified as StmSet
import Test.LinearLocks.Utils
import Test.Syd
-- | Doctests
--
-- >>> :{
-- >>> unit_mutexes_cannot_be_locked_in_wrong_order :: IO ()
-- >>> unit_mutexes_cannot_be_locked_in_wrong_order = do
-- >>> m1 <- Mutex.new 2 "hello"
-- >>> m2 <- Mutex.new 4 "world"
-- >>> lockScope \key -> L.do
-- >>> (mg2, key) <- Mutex.acquire key m2
-- >>> (mg1, key) <- Mutex.acquire key m1
-- >>> Mutex.release mg1
-- >>> Mutex.release mg2
-- >>> dropKeyAndReturn key ()
-- >>> :}
-- ...
-- ... • Cannot satisfy: 5 <= 2
-- ... • In a stmt of a 'do' block: (mg1, key) <- Mutex.acquire key m1
-- ...
spec :: Spec
spec = describe "Mutex" do
it "read mutex" do
mutex <- Mutex.new 0 "hello"
str <- lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
(Ur str, mg) <- Mutex.read mg
Mutex.release mg
dropKeyAndReturn key str
str `shouldBe` "hello"
it "write mutex" do
mutex <- Mutex.new 0 "hello"
lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
mg <- Mutex.write mg "world"
Mutex.release mg
dropKeyAndReturn key ()
str <- lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
(Ur str, mg) <- Mutex.read mg
Mutex.release mg
dropKeyAndReturn key str
str `shouldBe` "world"
str <- MVar.readMVar mutex.var
str `shouldBe` "world"
it "realeases mvar" do
mutex <- Mutex.new 0 "hello"
lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
L.liftSystemIO do
isEmpty <- MVar.isEmptyMVar mutex.var
isEmpty `shouldBe` True
Mutex.release mg
L.liftSystemIO do
isEmpty <- MVar.isEmptyMVar mutex.var
isEmpty `shouldBe` False
dropKeyAndReturn key ()
isEmpty <- MVar.isEmptyMVar mutex.var
isEmpty `shouldBe` False
it "can't nest lock scopes" do
let run =
lockScope \key -> L.do
L.liftSystemIO do
lockScope \key -> dropKeyAndReturn key ()
dropKeyAndReturn key ()
run `shouldThrow` \(_ :: NestedLocksScopeException) -> True
it "updates thread ids" do
let getThreadIds :: IO [ThreadId]
getThreadIds =
Internal.lockScopes & StmSet.listT & ListT.toList & atomically
tid <- myThreadId
getThreadIds >>= \tids -> tids `shouldNotContain` [tid]
lockScope \key -> L.do
L.liftSystemIO L.$ getThreadIds >>= \tids -> tids `shouldContain` [tid]
dropKeyAndReturn key ()
getThreadIds >>= \tids -> tids `shouldNotContain` [tid]
-- Check that the thread ID is removed even if an exception is thrown.
let run =
lockScope \key -> L.do
L.liftSystemIO L.$ getThreadIds >>= \tids -> tids `shouldContain` [tid]
L.liftSystemIO L.$ throwIO (userError "oops")
dropKeyAndReturn key ()
run `shouldThrow` anyIOException
getThreadIds >>= \tids -> tids `shouldNotContain` [tid]
-- Check that the thread ID is removed even if when a nested lock scope is attempted
let run =
lockScope \key -> L.do
L.liftSystemIO L.$ getThreadIds >>= \tids -> tids `shouldContain` [tid]
L.liftSystemIO do
lockScope \key -> dropKeyAndReturn key ()
dropKeyAndReturn key ()
run `shouldThrow` \(_ :: NestedLocksScopeException) -> True
getThreadIds >>= \tids -> tids `shouldNotContain` [tid]
-- Check that the thread ID is NOT removed if a nested lock scope is caught
lockScope \key -> L.do
L.liftSystemIO L.$ getThreadIds >>= \tids -> tids `shouldContain` [tid]
L.liftSystemIO do
Left _ <- try @SomeException $ lockScope \key -> dropKeyAndReturn key ()
pure ()
L.liftSystemIO L.$ getThreadIds >>= \tids -> tids `shouldContain` [tid]
dropKeyAndReturn key ()
getThreadIds >>= \tids -> tids `shouldNotContain` [tid]
it "rolls back on exception" do
mutex <- Mutex.new 0 "hello"
Left _ <- try @SomeException $ lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
mg <- Mutex.write mg "world"
L.liftSystemIO L.$ throwIO (userError "oops")
Mutex.release mg
dropKeyAndReturn key ()
-- The MVar should have been released, and the original value should have been put back into the MVar.
mbResult <- MVar.tryTakeMVar mutex.var
mbResult `shouldBe` Just "hello"
it "rolls back on imprecise exception" do
mutex <- Mutex.new 0 "hello"
Left _ <- try @SomeException $ lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
mg <- Mutex.write mg "world"
error "err"
Mutex.release mg
dropKeyAndReturn key ()
-- The MVar should have been released, and the original value should have been put back into the MVar.
mbResult <- MVar.tryTakeMVar mutex.var
mbResult `shouldBe` Just "hello"
it "new doesn't evaluate value to normal form" do
-- This should not throw, the "error" thunk should not be evaluated
void $ Mutex.new @[Int] 0 [1, 2, error "oops", 4]
it "release doesn't evaluate value to normal form" do
mutex <- Mutex.new @[Int] 0 [1]
lockScope \key -> L.do
(mg, key) <- Mutex.acquire key mutex
-- This should not throw, the "error" thunk should not be evaluated
mg <- Mutex.write mg [1, 2, error "oops", 4]
-- This should not throw
Mutex.release mg
dropKeyAndReturn key ()