158 lines
6.1 KiB
Haskell
158 lines
6.1 KiB
Haskell
module Test.Threading.Data.Mutex where
|
|
|
|
import Prelude
|
|
|
|
import Control.Monad.Error.Class (liftEither, liftMaybe)
|
|
import Control.Parallel (parOneOf)
|
|
import Data.Either (Either(..))
|
|
import Data.Maybe (isNothing)
|
|
import Data.Time.Duration (Milliseconds(..))
|
|
import Data.Traversable (for_)
|
|
import Effect.Aff as Aff
|
|
import Effect.Class (liftEffect)
|
|
import Effect.Exception (error)
|
|
import Effect.Ref as Ref
|
|
import Test.Spec (Spec, describe, it, pending')
|
|
import Test.Spec.Assertions (expectError, shouldEqual)
|
|
import Threading.Data.Mutex as Mutex
|
|
|
|
spec :: Spec Unit
|
|
spec =
|
|
describe "Threading.Data.Mutex" do
|
|
describe "mutex" do
|
|
it "creates" $ liftEffect $ void $ Mutex.mutex 0
|
|
describe "read" do
|
|
it "reads the value" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
v <- liftEffect $ Mutex.read g
|
|
v `shouldEqual` 0
|
|
it "throws if released" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
liftEffect $ Mutex.release g
|
|
expectError $ liftEffect $ Mutex.read g
|
|
describe "write" do
|
|
it "writes the value" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
liftEffect $ Mutex.write g 1
|
|
v <- liftEffect $ Mutex.read g
|
|
v `shouldEqual` 1
|
|
it "throws if released" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
liftEffect $ Mutex.release g
|
|
expectError $ liftEffect $ Mutex.write g 1
|
|
describe "get" do
|
|
it "yields immediately when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
val <- Mutex.get m
|
|
val `shouldEqual` 0
|
|
it "blocks until unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
getFiber <- Aff.forkAff $ Mutex.get m
|
|
liftEffect $ Mutex.write g 1
|
|
liftEffect $ Mutex.release g
|
|
read <- Aff.joinFiber getFiber
|
|
read `shouldEqual` 1
|
|
describe "put" do
|
|
it "yields immediately when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
Mutex.put m 1
|
|
val <- Mutex.get m
|
|
val `shouldEqual` 1
|
|
it "blocks until unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
getFiber <- Aff.forkAff $ Mutex.put m 2
|
|
liftEffect $ Mutex.write g 1
|
|
liftEffect $ Mutex.release g
|
|
Aff.joinFiber getFiber
|
|
val <- Mutex.get m
|
|
val `shouldEqual` 2
|
|
describe "modify" do
|
|
it "yields immediately when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
val <- Mutex.modify m (_ + 1)
|
|
val `shouldEqual` 1
|
|
it "blocks until unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
getFiber <- Aff.forkAff $ Mutex.modify m (_ * 10)
|
|
liftEffect $ Mutex.write g 1
|
|
liftEffect $ Mutex.release g
|
|
val <- Aff.joinFiber getFiber
|
|
val `shouldEqual` 10
|
|
describe "lock" do
|
|
it "yields immediately when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
void $ Mutex.lock m
|
|
it "blocks when locked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
finished <- liftEffect $ Ref.new false
|
|
fiber <- Aff.forkAff do
|
|
void $ Mutex.lock m
|
|
void $ liftEffect $ Ref.write true finished
|
|
Aff.delay $ Milliseconds 5.0
|
|
f1 <- liftEffect $ Ref.read finished
|
|
f1 `shouldEqual` false
|
|
liftEffect $ Mutex.release g
|
|
Aff.joinFiber fiber
|
|
f2 <- liftEffect $ Ref.read finished
|
|
f2 `shouldEqual` true
|
|
it "locks are acquired in the order they were requested" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- Mutex.lock m
|
|
a <- Aff.forkAff $ Mutex.modify_ m (_ + 1) -- 1
|
|
b <- Aff.forkAff $ Mutex.modify_ m (_ * 10) -- 10
|
|
c <- Aff.forkAff $ Mutex.modify_ m (_ + 10) -- 20
|
|
d <- Aff.forkAff $ Mutex.modify_ m (_ * 10) -- 200
|
|
liftEffect $ Mutex.release g
|
|
for_ [ a, b, c, d ] Aff.joinFiber
|
|
n <- Mutex.get m
|
|
n `shouldEqual` 200
|
|
pending' "should be (eventually) unlocked if a fiber exits without releasing the lock" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
-- Fiber acquires a lock then immediately resolves without releasing.
|
|
--
|
|
-- When the GC reclaims the guard object, the Mutex should notice and behave
|
|
-- as if it was explicitly released.
|
|
void $ Aff.forkAff $ void $ Mutex.lock m
|
|
liftEither =<< parOneOf
|
|
[ Aff.delay (Milliseconds 20000.0) $> Left (error "timed out waiting for GC to reclaim lock")
|
|
, Mutex.lock m $> Right unit
|
|
]
|
|
describe "tryLock" do
|
|
it "returns Just when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
void $ liftMaybe (error $ "Mutex.tryLock returned Nothing on new mutex") =<< liftEffect (Mutex.tryLock m)
|
|
it "returns Nothing when locked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
_ <- liftMaybe (error $ "Mutex.tryLock returned Nothing on new mutex") =<< liftEffect (Mutex.tryLock m)
|
|
g <- liftEffect (Mutex.tryLock m)
|
|
isNothing g `shouldEqual` true
|
|
it "returns Just after release" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- liftMaybe (error $ "Mutex.tryLock returned Nothing on new mutex") =<< liftEffect (Mutex.tryLock m)
|
|
liftEffect $ Mutex.release g
|
|
void $ liftMaybe (error $ "Mutex.tryLock returned Nothing after lock released") =<< liftEffect (Mutex.tryLock m)
|
|
describe "locked" do
|
|
it "is false when unlocked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
l <- liftEffect $ Mutex.locked m
|
|
l `shouldEqual` false
|
|
it "is true when locked" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
_ <- liftMaybe (error $ "Mutex.tryLock returned Nothing on new mutex") =<< liftEffect (Mutex.tryLock m)
|
|
l <- liftEffect $ Mutex.locked m
|
|
l `shouldEqual` true
|
|
it "is false after lock released" do
|
|
m <- liftEffect $ Mutex.mutex 0
|
|
g <- liftMaybe (error $ "Mutex.tryLock returned Nothing on new mutex") =<< liftEffect (Mutex.tryLock m)
|
|
liftEffect $ Mutex.release g
|
|
l' <- liftEffect $ Mutex.locked m
|
|
l' `shouldEqual` false
|