{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} module Test.Control.Concurrent.Class.MonadMVar where import Control.Concurrent.Class.MonadMVar import Control.Monad.Class.MonadAsync import Control.Monad.Class.MonadFork import Control.Monad.Class.MonadTime.SI import Control.Monad.Class.MonadTimer.SI import Data.Bifoldable (bifoldMap) import Data.Foldable (traverse_) import Data.Functor (void, ($>)) import Data.Maybe (isNothing) import Data.Monoid (All (..)) import Control.Monad.IOSim import Test.QuickCheck import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck (testProperty) tests :: TestTree tests = testGroup "Control.Concurrent.Class.MonadMVar" [ testGroup "putMVar" [ testProperty "fairness (IOSim)" prop_putMVar_fairness_sim , testCase "blocks on a full MVar (IOSim)" unit_putMVar_blocks_on_full_sim , testCase "blocks on a full MVar (IO)" unit_putMVar_blocks_on_full_io ] , testGroup "takeMVar" [ testProperty "fairness (IOSim)" prop_takeMVar_fairness_sim , testCase "blocks on an empty MVar (IOSim)" unit_takeMVar_blocks_on_empty_sim , testCase "blocks on an empty MVar (IO)" unit_takeMVar_blocks_on_empty_io ] , testGroup "tryTakeMVar" [ testCase "does not block on an empty MVar (IOSim)" unit_tryTakeMVar_empty , testCase "does not block on a full MVar (IOSim)" unit_tryTakeMVar_full , testCase "return value on an empty MVar (IOSim)" unit_tryTakeMVar_return_empty_sim , testCase "return value on an full MVar (IOSim)" unit_tryTakeMVar_return_full_sim ] , testGroup "tryPutMVar" [ testCase "does not block on an empty MVar (IOSim)" unit_tryPutMVar_empty , testCase "does not block on a full MVar (IOSim)" unit_tryPutMVar_full , testCase "return value on an empty MVar (IOSim)" unit_tryPutMVar_return_empty_sim , testCase "return value on an full MVar (IOSim)" unit_tryPutMVar_return_full_sim ] , testGroup "isEmptyMVar" [ testCase "empty MVar is empty" unit_isEmptyMVar_empty_sim , testCase "full MVar is not empty" unit_isEmptyMVar_full_sim ] ] -- -- putMVar -- -- | Check that 'takeMVar' is fair. This is test is only designed for 'IOSim' -- as it relies on its thread scheduling and determinism. -- putMVar_fairness_property :: forall m. ( MonadAsync m , MonadDelay m , MonadMVar m ) => Int -- ^ number of threads -> m Bool putMVar_fairness_property n = do v <- newEmptyMVar traverse_ (\a -> async $ do threadDelay 0.01 putMVar v a) [1..n] threadDelay 0.02 results <- sequence (replicate n (takeMVar v)) return $ results == [1..n] prop_putMVar_fairness_sim :: Positive (Small Int) -> Property prop_putMVar_fairness_sim (Positive (Small n)) = let trace = runSimTrace (putMVar_fairness_property n) in counterexample (ppTrace trace) $ case traceResult False trace of Left err -> counterexample (show err) False Right a -> property a unit_putMVar_blocks_on_full :: ( MonadFork m , MonadDelay m , MonadMVar m ) => m Bool unit_putMVar_blocks_on_full = do start <- getMonotonicTime let delta = 0.01 v <- newMVar () _ <- forkIO $ threadDelay delta >> takeMVar v $> () putMVar v () end <- getMonotonicTime return (end `diffTime` start >= delta) unit_putMVar_blocks_on_full_sim :: Assertion unit_putMVar_blocks_on_full_sim = assertBool "did not block on an full MVar" $ runSimOrThrow unit_putMVar_blocks_on_full unit_putMVar_blocks_on_full_io :: Assertion unit_putMVar_blocks_on_full_io = unit_putMVar_blocks_on_full >>= assertBool "did not block on an full MVar" -- -- takeMVar -- -- | Check that 'takeMVar' is fair. This is test is only designed for 'IOSim' -- as it relies on its thread scheduling and determinism. -- takeMVar_fairness_property :: forall m. ( MonadAsync m , MonadDelay m , MonadMVar m , Eq (Async m Int) ) => Int -- ^ number of threads -> m Property takeMVar_fairness_property n = do v <- newEmptyMVar ts <- sequence $ replicate n (async $ takeMVar v) threadDelay 0.01 traverse_ (putMVar v) [1..n] results <- waitAll ts return $ results === [1..n] prop_takeMVar_fairness_sim :: Positive (Small Int) -> Property prop_takeMVar_fairness_sim (Positive (Small n)) = runSimOrThrow (takeMVar_fairness_property n) unit_takeMVar_blocks_on_empty :: ( MonadFork m , MonadDelay m , MonadMVar m ) => m Bool unit_takeMVar_blocks_on_empty = do start <- getMonotonicTime let delta = 0.01 v <- newEmptyMVar _ <- forkIO $ threadDelay delta >> putMVar v () takeMVar v end <- getMonotonicTime return (end `diffTime` start >= delta) unit_takeMVar_blocks_on_empty_sim :: Assertion unit_takeMVar_blocks_on_empty_sim = assertBool "did not block on an empty MVar" $ runSimOrThrow unit_takeMVar_blocks_on_empty unit_takeMVar_blocks_on_empty_io :: Assertion unit_takeMVar_blocks_on_empty_io = unit_takeMVar_blocks_on_empty >>= assertBool "did not block on an empty MVar" -- -- tryTakeMVar -- -- | Check that `IOSim`'s `tryTakeMVar` is non blocking. -- tryTakeMVar_non_blocking_property :: Bool -> Bool tryTakeMVar_non_blocking_property isEmpty = validateTrace $ runSimTrace $ do v <- if isEmpty then newEmptyMVar else newMVar () void $ tryTakeMVar v where validateTrace :: SimTrace a -> Bool validateTrace = getAll . bifoldMap (const (All True)) (\ev -> case seType ev of EventTxBlocked {} -> All False _ -> All True) unit_tryTakeMVar_empty :: Assertion unit_tryTakeMVar_empty = assertBool "blocked on an empty MVar" $ tryTakeMVar_non_blocking_property False unit_tryTakeMVar_full :: Assertion unit_tryTakeMVar_full = assertBool "blocked on an empty MVar" $ tryTakeMVar_non_blocking_property True tryTakeMVar_return_value :: MonadMVar m => Bool -> m Bool tryTakeMVar_return_value isEmpty = do v <- if isEmpty then newEmptyMVar else newMVar () a <- tryTakeMVar v return $ isNothing a == isEmpty unit_tryTakeMVar_return_empty_sim :: Assertion unit_tryTakeMVar_return_empty_sim = assertBool "tryTakeMVar on an empty should return result" $ runSimOrThrow (tryTakeMVar_return_value True) unit_tryTakeMVar_return_full_sim :: Assertion unit_tryTakeMVar_return_full_sim = assertBool "tryTakeMVar on an full should return result" $ runSimOrThrow (tryTakeMVar_return_value False) -- -- tryPutMVar -- -- | Check that `IOSim`'s `tryPutMVar` is non blocking. -- tryPutMVar_non_blocking_property :: Bool -> Bool tryPutMVar_non_blocking_property isEmpty = validateTrace $ runSimTrace $ do v <- if isEmpty then newEmptyMVar else newMVar () void $ tryPutMVar v () where validateTrace :: SimTrace a -> Bool validateTrace = getAll . bifoldMap (const (All True)) (\ev -> case seType ev of EventTxBlocked {} -> All False _ -> All True) unit_tryPutMVar_empty :: Assertion unit_tryPutMVar_empty = assertBool "blocked on an empty MVar" $ tryPutMVar_non_blocking_property False unit_tryPutMVar_full :: Assertion unit_tryPutMVar_full = assertBool "blocked on an empty MVar" $ tryPutMVar_non_blocking_property True tryPutMVar_return_value :: forall m. MonadMVar m => Bool -> m Bool tryPutMVar_return_value isEmpty = do v :: MVar m () <- if isEmpty then newEmptyMVar else newMVar () a <- tryPutMVar v () return $ a == isEmpty unit_tryPutMVar_return_empty_sim :: Assertion unit_tryPutMVar_return_empty_sim = assertBool "tryPutMVar on an empty should return result" $ runSimOrThrow (tryPutMVar_return_value True) unit_tryPutMVar_return_full_sim :: Assertion unit_tryPutMVar_return_full_sim = assertBool "tryPutMVar on an full should return result" $ runSimOrThrow (tryPutMVar_return_value False) -- -- isEmptyMVar -- prop_isEmptyMVar :: forall m. MonadMVar m => Bool -> m Bool prop_isEmptyMVar isEmpty = do v :: MVar m () <- if isEmpty then newEmptyMVar else newMVar () (isEmpty ==) <$> isEmptyMVar v unit_isEmptyMVar_empty_sim :: Assertion unit_isEmptyMVar_empty_sim = assertBool "empty mvar must be empty" $ runSimOrThrow (prop_isEmptyMVar True) unit_isEmptyMVar_full_sim :: Assertion unit_isEmptyMVar_full_sim = assertBool "full mvar must not be empty" $ runSimOrThrow (prop_isEmptyMVar False) -- -- Utils -- waitAll :: forall m. ( MonadAsync m , Eq (Async m Int) ) => [Async m Int] -> m [Int] waitAll = go [] where go :: [Int] -> [Async m Int] -> m [Int] go as ts = do (t, a) <- waitAny ts let ts' = filter (/= t) ts case ts' of [] -> return (reverse (a : as)) _ -> go (a : as) ts'