linear-locks-0.1.0.1: test/Test/LinearLocks/RWLockSpec.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE NoFieldSelectors #-}
module Test.LinearLocks.RWLockSpec where
import Control.Concurrent.ReadWriteLock qualified as Conc
import Control.Exception (SomeException, throwIO, try)
import Control.Functor.Linear qualified as L
import Control.Monad (void, when)
import Control.Monad.IO.Class.Linear qualified as L
import Data.IORef qualified as IORef
import LinearLocks
import LinearLocks.Internal.RWLock qualified as Internal
import LinearLocks.RWLock qualified as RWLock
import Prelude.Linear (Ur (..))
import Prelude.Linear qualified as L hiding (IO)
import Test.Syd
-- | Doctests
--
-- >>> :{
-- >>> unit_mutexes_cannot_be_locked_in_wrong_order :: IO ()
-- >>> unit_mutexes_cannot_be_locked_in_wrong_order = do
-- >>> m1 <- RWLock.new 2 "hello"
-- >>> m2 <- RWLock.new 4 "world"
-- >>> lockScope \key -> L.do
-- >>> (g2, key) <- RWLock.acquireRead key m2
-- >>> (g1, key) <- RWLock.acquireRead key m1
-- >>> RWLock.releaseRead g1
-- >>> RWLock.releaseRead g2
-- >>> dropKeyAndReturn key ()
-- >>> :}
-- ...
-- ... • Cannot satisfy: 5 <= 2
-- ... • In a stmt of a 'do' block: (g1, key) <- RWLock.acquireRead key m1
-- ...
spec :: Spec
spec = describe "RWLock" do
it "read mutex" do
rwl <- RWLock.new 0 "hello"
-- Read in "read mode"
str <- lockScope \key -> L.do
(guard, key) <- RWLock.acquireRead key rwl
(Ur str, guard) <- RWLock.read guard
RWLock.releaseRead guard
dropKeyAndReturn key str
str `shouldBe` "hello"
-- Read in "write mode"
str <- lockScope \key -> L.do
(guard, key) <- RWLock.acquireWrite key rwl
(Ur str, guard) <- RWLock.read guard
RWLock.releaseWrite guard
dropKeyAndReturn key str
str `shouldBe` "hello"
it "write mutex" do
rwl <- RWLock.new 0 "hello"
-- Write in "write mode"
lockScope \key -> L.do
(guard, key) <- RWLock.acquireWrite key rwl
guard <- RWLock.write guard "world"
RWLock.releaseWrite guard
dropKeyAndReturn key ()
-- Read in "read mode"
str <- lockScope \key -> L.do
(guard, key) <- RWLock.acquireRead key rwl
(Ur str, guard) <- RWLock.read guard
RWLock.releaseRead guard
dropKeyAndReturn key str
str `shouldBe` "world"
-- Read in "write mode"
str <- lockScope \key -> L.do
(guard, key) <- RWLock.acquireWrite key rwl
(Ur str, guard) <- RWLock.read guard
RWLock.releaseWrite guard
dropKeyAndReturn key str
str `shouldBe` "world"
str <- IORef.readIORef rwl.var
str `shouldBe` "world"
it "realeases ioref in read mode" do
rwl <- RWLock.new 0 "hello"
lockScope \key -> L.do
(mg, key) <- RWLock.acquireRead key rwl
-- If the lock was acquired in "read mode",
-- we shouldn't be able to acquire it again in "write mode",
-- but we should be able to acquire it in "read mode".
L.liftSystemIO do
assertCanRead rwl True
assertCanWrite rwl False
RWLock.releaseRead mg
-- The lock was released, we should be able to acquire it in both "read mode" and "write mode".
L.liftSystemIO do
assertCanRead rwl True
assertCanWrite rwl True
dropKeyAndReturn key ()
-- The lock was released, we should be able to acquire it in both "read mode" and "write mode".
assertCanRead rwl True
assertCanWrite rwl True
it "realeases ioref in write mode" do
rwl <- RWLock.new 0 "hello"
lockScope \key -> L.do
(mg, key) <- RWLock.acquireWrite key rwl
-- If the lock was acquired in "write mode",
-- we shouldn't be able to acquire it again in "write mode" or "read mode".
L.liftSystemIO do
assertCanRead rwl False
assertCanWrite rwl False
RWLock.releaseWrite mg
-- The lock was released, we should be able to acquire it in both "read mode" and "write mode".
L.liftSystemIO do
assertCanRead rwl True
assertCanWrite rwl True
dropKeyAndReturn key ()
-- The lock was released, we should be able to acquire it in both "read mode" and "write mode".
assertCanRead rwl True
assertCanWrite rwl True
it "rolls back on exception" do
rwl <- RWLock.new 0 "hello"
Left _ <- try @SomeException $ lockScope \key -> L.do
(mg, key) <- RWLock.acquireWrite key rwl
mg <- RWLock.write mg "world"
L.liftSystemIO L.$ throwIO (userError "oops")
RWLock.releaseWrite mg
dropKeyAndReturn key ()
-- The IORef should have been released, and the original value should have been put back into the IORef.
assertCanRead rwl True
assertCanWrite rwl True
mbResult <- IORef.readIORef rwl.var
mbResult `shouldBe` "hello"
it "rolls back on imprecise exception" do
rwl <- RWLock.new 0 "hello"
Left _ <- try @SomeException $ lockScope \key -> L.do
(mg, key) <- RWLock.acquireWrite key rwl
mg <- RWLock.write mg "world"
error "err"
RWLock.releaseWrite mg
dropKeyAndReturn key ()
-- The IORef should have been released, and the original value should have been put back into the IORef.
assertCanRead rwl True
assertCanWrite rwl True
mbResult <- IORef.readIORef rwl.var
mbResult `shouldBe` "hello"
it "new doesn't evaluate value to normal form" do
-- This should not throw, the "error" thunk should not be evaluated
void $ RWLock.new @[Int] 0 [1, 2, error "oops", 4]
it "release doesn't evaluate value to normal form" do
mutex <- RWLock.new @[Int] 0 [1]
lockScope \key -> L.do
(mg, key) <- RWLock.acquireWrite key mutex
-- This should not throw, the "error" thunk should not be evaluated
mg <- RWLock.write mg [1, 2, error "oops", 4]
-- This should not throw
RWLock.releaseWrite mg
dropKeyAndReturn key ()
assertCanRead :: RWLock.RWLock lvl a -> Bool -> IO ()
assertCanRead rwl expected = do
canRead <- Conc.tryAcquireRead rwl.lock
canRead `shouldBe` expected
-- Release the lock if it was acquired.
when canRead do
Conc.releaseRead rwl.lock
assertCanWrite :: RWLock.RWLock lvl a -> Bool -> IO ()
assertCanWrite rwl expected = do
canWrite <- Conc.tryAcquireWrite rwl.lock
canWrite `shouldBe` expected
-- Release the lock if it was acquired.
when canWrite do
Conc.releaseWrite rwl.lock