packages feed

linear-locks-0.1.0.0: test/Test/LinearLocks/MutexSpec.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE PackageImports #-}
{-# 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.Hspec.Expectations.Pretty (anyIOException, shouldThrow)
import "tasty-hunit-compat" Test.Tasty.HUnit

-- | 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
-- ...
unit_read_mutex :: IO ()
unit_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 @?= "hello"

unit_write_mutex :: IO ()
unit_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 @?= "world"

  str <- MVar.readMVar mutex.var
  str @?= "world"

unit_realeases_mvar :: IO ()
unit_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 @?= True

    Mutex.release mg

    L.liftSystemIO do
      isEmpty <- MVar.isEmptyMVar mutex.var
      isEmpty @?= False

    dropKeyAndReturn key ()

  isEmpty <- MVar.isEmptyMVar mutex.var
  isEmpty @?= False

unit_cant_nest_lockscopes :: IO ()
unit_cant_nest_lockscopes = do
  let run =
        lockScope \key -> L.do
          L.liftSystemIO do
            lockScope \key -> dropKeyAndReturn key ()
          dropKeyAndReturn key ()

  run `shouldThrow` \(_ :: NestedLocksScopeException) -> True

unit_updates_thread_ids :: IO ()
unit_updates_thread_ids = do
  tid <- myThreadId

  getThreadIds >>= \tids -> tids @?= []
  lockScope \key -> L.do
    L.liftSystemIO L.$ getThreadIds >>= \tids -> tids @?= [tid]
    dropKeyAndReturn key ()
  getThreadIds >>= \tids -> tids @?= []

  -- 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 @?= [tid]
          L.liftSystemIO L.$ throwIO (userError "oops")
          dropKeyAndReturn key ()
  run `shouldThrow` anyIOException
  getThreadIds >>= \tids -> tids @?= []

  -- 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 @?= [tid]
          L.liftSystemIO do
            lockScope \key -> dropKeyAndReturn key ()
          dropKeyAndReturn key ()
  run `shouldThrow` \(_ :: NestedLocksScopeException) -> True
  getThreadIds >>= \tids -> tids @?= []

  -- 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 @?= [tid]
    L.liftSystemIO do
      Left _ <- try @SomeException $ lockScope \key -> dropKeyAndReturn key ()
      pure ()
    L.liftSystemIO L.$ getThreadIds >>= \tids -> tids @?= [tid]
    dropKeyAndReturn key ()
  getThreadIds >>= \tids -> tids @?= []
  where
    getThreadIds :: IO [ThreadId]
    getThreadIds =
      Internal.lockScopes & StmSet.listT & ListT.toList & atomically

unit_rolls_back_on_exception :: IO ()
unit_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 @?= Just "hello"

unit_rolls_back_on_imprecise_exception :: IO ()
unit_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 @?= Just "hello"

unit_new_doesnt_evaluate_value_to_normal_form :: IO ()
unit_new_doesnt_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]

unit_release_doesnt_evaluate_value_to_normal_form :: IO ()
unit_release_doesnt_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 ()