linear-locks-0.1.0.1: test/Test/LinearLocks/StrictRWLockSpec.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE NoFieldSelectors #-}
module Test.LinearLocks.StrictRWLockSpec where
import Control.Concurrent.MVar qualified as MVar
import Control.Concurrent.ReadWriteLock qualified as Conc
import Control.Exception (SomeException, throwIO, try)
import Control.Functor.Linear qualified as L
import Control.Monad (when)
import Control.Monad.IO.Class.Linear qualified as L
import Data.IORef qualified as IORef
import LinearLocks
import LinearLocks.Internal qualified as Internal
import LinearLocks.Internal.StrictRWLock qualified as Internal
import LinearLocks.RWLock.Strict qualified as RWLock
import Prelude.Linear (Ur (..))
import Prelude.Linear qualified as L hiding (IO)
import System.IO.Resource.Linear (RIO)
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 "Strict 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.unNF `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.unNF `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.unNF `shouldBe` "hello"
it "new evaluates value to normal form" do
RWLock.new @[Int] 0 [1, 2, error "oops", 4]
`shouldThrow` errorCall "oops"
it "release evaluates value to normal form" do
mutex <- RWLock.new @[Int] 0 [1]
logs <- MVar.newMVar @[String] []
let logMsg :: String -> RIO ()
logMsg msg = L.liftSystemIO do
MVar.modifyMVar_ logs \logs -> pure (logs <> [msg])
let run =
lockScope \key -> L.do
(mg, key) <- RWLock.acquireWrite key mutex
logMsg "ran 'acquire'"
mg <- RWLock.write mg [1, 2, error "oops", 4]
logMsg "ran 'write'"
RWLock.releaseWrite mg
logMsg "ran 'release'"
dropKeyAndReturn key ()
run `shouldThrow` errorCall "oops"
-- The exception should be thrown WHILE running `release`.
-- `write` should NOT throw.
msgs <- MVar.takeMVar logs
msgs `shouldBe` ["ran 'acquire'", "ran 'write'"]
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