{-# LANGUAGE CPP #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} module Control.SchedulerSpec ( spec ) where import Data.Int import Control.Concurrent (killThread, myThreadId, threadDelay, yield) import Control.Concurrent.MVar import Control.DeepSeq import qualified Control.Exception as EUnsafe import Control.Exception.Base (ArithException(DivideByZero), AsyncException(ThreadKilled)) import Control.Monad import Control.Monad.ST import Control.Scheduler as S import Data.Bits (complement) import qualified Data.Foldable as F (toList, traverse_) import Data.IORef import Data.List (groupBy, sort, sortOn) import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Test.QuickCheck.Function import Test.QuickCheck.Monadic import Test.Validity.Eq import Test.Validity.Functor import Test.Validity.Monoid import Test.Validity.Ord import Test.Validity.Show import UnliftIO.Async import UnliftIO.Exception hiding (assert) #if !MIN_VERSION_base(4,11,0) import Data.Semigroup #endif concurrentProperty :: Testable prop => prop -> Property concurrentProperty = within 2000000 concurrentExpectation :: Expectation -> Property concurrentExpectation = concurrentProperty concurrentPropertyIO :: Testable prop => IO prop -> Property concurrentPropertyIO = concurrentProperty . monadicIO . run instance Arbitrary Comp where arbitrary = frequency [(20, pure Seq), (80, getNonSeq <$> arbitrary)] newtype NonSeq = NonSeq {getNonSeq :: Comp } deriving (Show, Eq) instance Arbitrary NonSeq where arbitrary = NonSeq <$> frequency [(10, pure Par), (35, ParOn <$> arbitrary), (35, ParN . getSmall <$> arbitrary)] newtype SeqLike = SeqLike {getSeqLike :: Comp } deriving (Show, Eq) instance Arbitrary SeqLike where arbitrary = SeqLike <$> oneof [pure Seq, ParOn . pure <$> arbitrary, pure $ ParN 1] prop_SameList :: Comp -> [Int] -> Property prop_SameList comp xs = concurrentPropertyIO $ do xs' <- withScheduler comp $ \scheduler -> mapM_ (scheduleWork scheduler . return) xs return (xs === xs') prop_Recursive :: Comp -> [Int] -> Property prop_Recursive comp xs = concurrentPropertyIO $ do xs' <- withScheduler comp (schedule xs) return (sort xs === sort xs') where schedule [] _ = return () schedule (y:ys) scheduler = scheduleWork scheduler (schedule ys scheduler >> return y) prop_Serially :: Comp -> [Int] -> Property prop_Serially comp xs = concurrentPropertyIO $ do xs' <- schedule xs return (xs === concat xs') where schedule [] = return [] schedule (y:ys) = do y' <- withScheduler comp (`scheduleWork` pure y) ys' <- schedule ys return (y':ys') prop_Nested :: Comp -> [Int] -> Property prop_Nested comp xs = concurrentPropertyIO $ do xs' <- schedule xs return (sort xs === sort (concat xs')) where schedule [] = return [] schedule (y:ys) = withScheduler comp (\s -> scheduleWork s (schedule ys >>= \ys' -> return (y : concat ys'))) -- | Check whether all jobs have been completed (similar roprop_Traverse) prop_AllJobsProcessed :: Comp -> [Int] -> Property prop_AllJobsProcessed comp jobs = concurrentProperty $ monadicIO ((=== jobs) <$> run (withScheduler comp $ \scheduler -> mapM_ (scheduleWork scheduler . pure) jobs)) prop_Traverse :: Comp -> [Int] -> Fun Int Int -> Property prop_Traverse comp xs f = concurrentPropertyIO $ (===) <$> traverse f' xs <*> traverseConcurrently comp f' xs where f' = pure . apply f replicateLike :: ([Word] -> [Word]) -> (Int -> IO Word -> IO [Word]) -> Int -> Fun Word Word -> IO () replicateLike adjust justAs n f = do iRef <- newIORef 0 jRef <- newIORef 0 let g ref = atomicModifyIORef' ref (\i -> (apply f i, i + 1)) xs <- replicateM n (g jRef) ys <- justAs n (g iRef) adjust ys `shouldBe` adjust xs prop_ReplicateM :: ([Word] -> [Word]) -> Comp -> Int -> Fun Word Word -> Property prop_ReplicateM adjust comp i = concurrentPropertyIO . replicateLike adjust (S.replicateConcurrently comp) i prop_ReplicateWork :: ([Word] -> [Word]) -> Comp -> Int -> Fun Word Word -> Property prop_ReplicateWork adjust comp i = concurrentPropertyIO . replicateLike adjust (\n g -> withScheduler comp (\s -> replicateWork s n g)) i prop_ReplicateWork_ :: ([Word] -> [Word]) -> Comp -> Int -> Fun Word Word -> Property prop_ReplicateWork_ adjust comp i = concurrentPropertyIO . replicateLike adjust scheduleAndCollect i where scheduleAndCollect n g = do ref <- newIORef [] withScheduler_ comp $ \s -> replicateWork_ s n $ do x <- g atomicModifyIORef' ref (\xs -> (x : xs, ())) reverse <$> readIORef ref prop_ManyJobsInChunks :: Property prop_ManyJobsInChunks = noShrinking $ \ comp (jss :: [[Int]]) -> concurrentExpectation $ do xs <- withScheduler comp $ \s -> forM_ jss $ \js -> do rs <- runBatch s $ \ _ -> mapM_ (scheduleWork s . pure) js rs `shouldBe` js xs `shouldBe` [] prop_ArbitraryCompNested :: [(Comp, Int)] -> Property prop_ArbitraryCompNested xs = concurrentPropertyIO $ do xs' <- schedule xs return (sort (map snd xs) === sort (concat xs')) where schedule [] = return [] schedule ((c, y):ys) = withScheduler c (\s -> scheduleWork s (schedule ys >>= \ys' -> return (y : concat ys'))) -- | Ensure proper exception handling. prop_CatchDivideByZero :: Comp -> Int -> [Positive Int] -> Property prop_CatchDivideByZero comp k xs = concurrentProperty $ assertExceptionIO (== DivideByZero) (traverseConcurrently comp (\i -> return (k `div` i)) (map getPositive xs ++ [0] ++ map getPositive xs)) -- | Ensure proper exception handling. prop_CatchDivideByZeroNested :: Comp -> Int -> Positive Int -> Property prop_CatchDivideByZeroNested comp a (Positive k) = concurrentProperty $ assertExceptionIO (== DivideByZero) (schedule k) where schedule i | i < 0 = return [] | otherwise = withScheduler comp (\s -> scheduleWork s (schedule (i - 1) >> return (a `div` i))) -- | Make sure one co-worker can kill another one, of course when there are at least two of. prop_KillBlockedCoworker :: Comp -> Property prop_KillBlockedCoworker comp = concurrentProperty $ assertExceptionIO (== DivideByZero) (withScheduler_ comp $ \scheduler -> if numWorkers scheduler < 2 then scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int)) else do mv <- newEmptyMVar scheduleWork scheduler $ readMVar mv scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int))) -- | Make sure one co-worker can kill another one, of course when there are at least two of. prop_KillSleepingCoworker :: Comp -> Property prop_KillSleepingCoworker comp = concurrentProperty $ assertExceptionIO (== DivideByZero) (withScheduler_ comp $ \scheduler -> do scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int)) scheduleWork scheduler $ do threadDelay 500000 error "This should never happen! Thread should have been killed by now.") prop_ExpectAsyncException :: Comp -> Property prop_ExpectAsyncException comp = concurrentProperty $ let didAWorkerDie = EUnsafe.handleJust EUnsafe.asyncExceptionFromException (return . (== EUnsafe.ThreadKilled)) . fmap or in (monadicIO . run . didAWorkerDie . withScheduler comp $ \s -> scheduleWork s (myThreadId >>= killThread >> pure False)) .&&. (monadicIO . run . fmap not . didAWorkerDie . withScheduler Par $ \s -> scheduleWork s $ pure False) prop_WorkerCaughtAsyncException :: Positive Int -> Property prop_WorkerCaughtAsyncException (Positive n) = concurrentProperty $ assertExceptionIO (== DivideByZero) $ do lock <- newEmptyMVar result <- race (readMVar lock) $ withScheduler_ (ParN 2) $ \scheduler -> do scheduleWork scheduler $ do threadDelay (n `mod` 1000000) EUnsafe.throwIO DivideByZero scheduleWork scheduler $ do e <- tryAny $ replicateM_ 5 $ threadDelay 1000000 case e of Right _ -> throwString "Impossible, shouldn't have waited for so long" Left exc -> do putMVar lock exc throwString $ "I should not have survived: " ++ displayException (exc :: SomeException) void $ throwString $ case result of Left innerError -> "Scheduled job cought async exception: " ++ displayException innerError Right () -> "Scheduler terminated properly. Should not have happened" -- | Make sure there is no problems if sub-schedules worker get killed prop_AllWorkersDied :: Comp -> Comp -> Positive Int -> Property prop_AllWorkersDied comp1 comp (Positive n) = concurrentProperty $ assertAsyncExceptionIO (== ThreadKilled) (withScheduler_ comp1 $ \scheduler1 -> scheduleWork scheduler1 (withScheduler_ comp $ \scheduler -> replicateM_ n (scheduleWork scheduler (myThreadId >>= killThread)))) prop_FinishEarly_ :: NonSeq -> Property prop_FinishEarly_ (NonSeq comp) = concurrentPropertyIO $ do ref <- newIORef True withScheduler_ comp $ \scheduler -> scheduleWork_ scheduler (terminate_ scheduler >> yield >> threadDelay 10000 >> writeIORef ref False) counterexample "Scheduler did not terminate early" <$> readIORef ref prop_FinishEarly :: Comp -> Property prop_FinishEarly comp = concurrentPropertyIO $ do let scheduleJobs scheduler = do scheduleWork scheduler (pure (2 :: Int)) scheduleWork scheduler (threadDelay 10000 >> terminate scheduler 3 >> pure 1) res <- withScheduler comp scheduleJobs res' <- withSchedulerR comp scheduleJobs pure (res === [2, 3] .&&. res' === FinishedEarly [2] 3) prop_FinishEarlyWith :: Comp -> Int -> Property prop_FinishEarlyWith comp n = concurrentPropertyIO $ do let scheduleJobs scheduler = do scheduleWork scheduler $ pure (complement (n + 1)) scheduleWork scheduler $ terminateWith scheduler n >> pure (complement n) res <- withScheduler comp scheduleJobs res' <- withSchedulerR comp scheduleJobs pure (res === [n] .&&. res' === FinishedEarlyWith n) prop_FinishBeforeStarting :: Comp -> Property prop_FinishBeforeStarting comp = concurrentPropertyIO $ do res <- withScheduler comp $ \scheduler -> do void $ terminate scheduler 1 scheduleWork scheduler (threadDelay 10000 >> pure 2) pure (res === [1 :: Int]) prop_FinishWithBeforeStarting :: Comp -> Int -> Property prop_FinishWithBeforeStarting comp n = concurrentPropertyIO $ do res <- withScheduler comp $ \scheduler -> do void $ terminateWith scheduler n scheduleWork scheduler $ pure (complement n) pure (res === [n]) prop_TrivialSchedulerSameAsSeq_ :: SeqLike -> [Int] -> Property prop_TrivialSchedulerSameAsSeq_ (SeqLike comp) zs = concurrentPropertyIO $ do let consRef xsRef x = atomicModifyIORef' xsRef $ \ xs -> (x:xs, ()) trivial = trivialScheduler_ nRef <- newIORef False xRefs <- newIORef [] yRefs <- newIORef [] withScheduler_ comp $ \scheduler -> do writeIORef nRef (numWorkers scheduler == numWorkers trivial) mapM_ (scheduleWork_ scheduler . consRef xRefs) zs mapM_ (scheduleWork_ trivial . consRef yRefs) zs nSame <- readIORef nRef xs <- readIORef xRefs ys <- readIORef yRefs pure (nSame .&&. xs === ys) prop_SameAsTrivialScheduler :: Comp -> [Int] -> Fun Int Int -> Property prop_SameAsTrivialScheduler comp zs f = concurrentPropertyIO $ do let schedule scheduler = forM_ zs (scheduleWork scheduler . pure . apply f) xs <- withScheduler comp schedule ys <- withTrivialScheduler schedule pure (xs === ys) prop_Terminate :: (Show a, Eq a) => ((Scheduler RealWorld Int -> IO ()) -> IO a) -> (Scheduler RealWorld Int -> Int -> IO Int) -> ([Int] -> Int -> a) -> [Int] -> Int -> [Int] -> Property prop_Terminate withSchedulerR' term expected xs x ys = concurrentExpectation $ do rs <- withSchedulerR' $ \scheduler -> do forM_ xs (scheduleWork scheduler . pure) _ <- scheduleWork scheduler $ term scheduler x forM_ ys (scheduleWork scheduler . pure) rs `shouldBe` expected xs x -- prop_TerminateSeq :: -- ((Scheduler IO Int -> IO ()) -> IO (Results Int)) -> [Int] -> Int -> [Int] -> Expectation -- prop_TerminateSeq withSchedulerR' xs x ys = do -- rs <- withSchedulerR' $ \ scheduler -> do -- forM_ xs (scheduleWork scheduler . pure) -- _ <- scheduleWork scheduler $ terminate scheduler x -- forM_ ys (scheduleWork scheduler . pure) -- rs `shouldBe` FinishedEarly xs x -- prop_TerminateWithSeq :: -- ((Scheduler IO Int -> IO ()) -> IO (Results Int)) -> [Int] -> Int -> [Int] -> Expectation -- prop_TerminateWithSeq withSchedulerR' xs x ys = do -- rs <- withSchedulerR' $ \ scheduler -> do -- forM_ xs (scheduleWork scheduler . pure) -- _ <- scheduleWork scheduler $ terminateWith scheduler x -- forM_ ys (scheduleWork scheduler . pure) -- rs `shouldBe` FinishedEarlyWith x newtype Elem = Elem Int deriving (Eq, Show) instance Exception Elem -- | Check if an element is in the list with an exception prop_TraverseConcurrently_ :: Comp -> [Int] -> Int -> Property prop_TraverseConcurrently_ comp xs x = concurrentPropertyIO $ do let f i | i == x = throwIO $ Elem x | otherwise = pure () eRes :: Either Elem () <- try $ traverse_ f xs eRes' <- try $ traverseConcurrently_ comp f xs return (eRes === eRes') -- TODO: fix the infinite property for single worker schedulers -- | Check if an element is in the list with an exception, where we know that list is infinite and -- element is part of that list. prop_TraverseConcurrentlyInfinite_ :: NonSeq -> [Int] -> Int -> Property prop_TraverseConcurrentlyInfinite_ (NonSeq comp) xs x = concurrentPropertyIO $ do let f i | i == x = throwIO $ Elem x | otherwise = pure () xs' = xs ++ [x] -- ++ [0 ..] eRes :: Either Elem () <- try $ F.traverse_ f xs' eRes' <- try $ traverseConcurrently_ comp f xs' return (eRes === eRes') prop_WorkerStateExclusive :: Comp -> NonNegative Int -> Property prop_WorkerStateExclusive comp (NonNegative n) = concurrentExpectation $ do state <- initWorkerStates comp (\wid -> (,) wid <$> newIORef (0 :: Int)) workerStatesComp state `shouldBe` comp nWorkers <- getCompWorkers comp let scheduleJobs schedulerWS = do replicateM n $ scheduleWorkState schedulerWS $ \(wid, ref) -> do counter <- readIORef ref writeIORef ref (counter + 1) pure (wid, counter) gather = map (sortOn snd) . groupBy (\x y -> fst x == fst y) . sortOn fst isMonotonicStartingAt _ [] = True isMonotonicStartingAt k (k':ks) = k == k' && isMonotonicStartingAt (k + 1) ks baseIds = [(wid, -1) | wid <- [0 .. WorkerId nWorkers - 1]] ids <- withSchedulerWS state scheduleJobs length ids `shouldBe` n let gathered = gather (ids ++ baseIds) map (map snd) gathered `shouldSatisfy` all (isMonotonicStartingAt (-1)) ids' <- withSchedulerWSR state scheduleJobs length ids' `shouldBe` n let gathered' = gather (baseIds ++ ids ++ F.toList ids') map (map snd) gathered' `shouldSatisfy` all (isMonotonicStartingAt (-1)) withSchedulerWS_ state $ \schedulerWS -> do numWorkers (unwrapSchedulerWS schedulerWS) `shouldBe` nWorkers replicateM (10 * n) $ scheduleWorkState_ schedulerWS $ \(wid, ref) -> do counter <- readIORef ref when (counter > 0) $ snd (last (gathered' !! getWorkerId wid)) `shouldBe` pred counter prop_MutexException :: Comp -> Property prop_MutexException comp = concurrentProperty $ assertExceptionIO (== MutexException) $ do state <- initWorkerStates comp (pure . getWorkerId) withSchedulerWS_ state $ \schedulerWS -> scheduleWorkState_ schedulerWS $ \_s -> withSchedulerWS_ state $ \_s' -> pure () prop_FindCancelResume :: Comp -> Int64 -> ([Int64], [Int64]) -> [Int64] -> Property prop_FindCancelResume comp x' (xs1', xs2') ys = concurrentExpectation $ do let f = (10 *) g = (100 *) xs1 = filter (/= x') xs1' xs2 = filter (/= x') xs2' xs = concat [xs1, [x'], xs2] res <- withSchedulerR comp $ \s -> do r <- runBatchR s $ \batch -> do forM_ xs $ \x -> scheduleWork s $ do if x == x' then Just x <$ cancelBatchWith batch (Just (f x)) else pure Nothing r `shouldBe` FinishedEarlyWith (Just (f x')) r' <- runBatchR s $ \_batch -> forM_ ys (scheduleWork s . pure . Just) r' `shouldBe` Finished (map Just ys) batch <- getCurrentBatch s forM_ xs $ \x -> scheduleWork s $ do if x == x' then Just x <$ cancelBatch batch (Just (g x)) else pure $ Just (f x) case res of FinishedEarly rs r -> do r `shouldBe` Just (g x') rs `satisfyOrderedPartialPrefix` concat [map (Just . f) xs1, [Just x'], map (Just . f) xs2] fr -> expectationFailure $ "Unexpected result: " ++ show fr where satisfyOrderedPartialPrefix as bs = unless (orderedPartialPrefixOf as bs) $ expectationFailure $ "Expected " ++ show as ++ " to be prefix of " ++ show bs ++ " possibly with some elements skipped" -- Make sure the first list is the prefix of the second orderedPartialPrefixOf [] _ = True orderedPartialPrefixOf (_:_) [] = False orderedPartialPrefixOf (a:as) (b:bs) | a == b = orderedPartialPrefixOf as bs | otherwise = orderedPartialPrefixOf (a : as) bs -- prop_CancelBatchEarly_ :: NonSeq -> Property -- prop_CancelBatchEarly_ (NonSeq comp) = -- concurrentPropertyIO $ do -- ref <- newIORef True -- withScheduler_ comp $ \scheduler -> -- scheduleWork_ -- scheduler -- (cancelBatch_ scheduler >> yield >> threadDelay 10000 >> writeIORef ref False) -- counterexample "Scheduler did not terminate early" <$> readIORef ref -- prop_FinishEarly :: Comp -> Property -- prop_FinishEarly comp = -- concurrentPropertyIO $ do -- let scheduleJobs scheduler = do -- scheduleWork scheduler (pure (2 :: Int)) -- scheduleWork scheduler (threadDelay 10000 >> terminate scheduler 3 >> pure 1) -- res <- withScheduler comp scheduleJobs -- res' <- withSchedulerR comp scheduleJobs -- pure (res === [2, 3] .&&. res' === FinishedEarly [2] 3) -- prop_FinishEarlyWith :: Comp -> Int -> Property -- prop_FinishEarlyWith comp n = -- concurrentPropertyIO $ do -- let scheduleJobs scheduler = do -- scheduleWork scheduler $ pure (complement (n + 1)) -- scheduleWork scheduler $ terminateWith scheduler n >> pure (complement n) -- res <- withScheduler comp scheduleJobs -- res' <- withSchedulerR comp scheduleJobs -- pure (res === [n] .&&. res' === FinishedEarlyWith n) spec :: Spec spec = do describe "Comp" $ do describe "Monoid" $ do it "x <> mempty = x" $ property $ \(x :: Comp) -> x <> mempty === x it "mempty <> x = x" $ property $ \(x :: Comp) -> mempty <> x === x it "x <> (y <> z) = (x <> y) <> z" $ property $ \(x :: Comp) y z -> x <> (y <> z) === (x <> y) <> z it "mconcat = foldr '(<>)' mempty" $ property $ \(xs :: [Comp]) -> mconcat xs === foldr (<>) mempty xs eqSpecOnArbitrary @Comp monoidSpecOnArbitrary @Comp describe "Show" $ do it "show == showsPrec 0" $ property $ \(x :: Comp) -> x `deepseq` show x === showsPrec 0 x "" it "(show) == showsPrec 1" $ property $ \(x :: Comp) (Positive n) -> x /= Seq && x /= Par ==> ("(" <> show x <> ")" === showsPrec n x "") describe "Results" $ do eqSpecOnArbitrary @(Results Int) functorSpecOnArbitrary @Results showReadSpecOnArbitrary @(Results Int) it "Traversable" $ property $ \(rs :: Results Int) (f :: Fun Int (Maybe Int)) -> traverse (apply f) (F.toList rs) === fmap F.toList (traverse (apply f) rs) describe "WorkerId" $ do eqSpecOnArbitrary @WorkerId ordSpecOnArbitrary @WorkerId it "MaxMin" $ property $ \x y -> conjoin [ max (WorkerId x) (WorkerId y) === WorkerId (max x y) , min (WorkerId x) (WorkerId y) === WorkerId (min x y) ] showReadSpecOnArbitrary @WorkerId describe "Enum" $ do it "toEnumFromEnum" $ property $ \ wid@(WorkerId i) -> toEnum (getWorkerId wid) === wid .&&. fromEnum wid === i it "succ . pred" $ property $ \ wid@(WorkerId i) -> i /= minBound && i /= maxBound ==> succ (pred wid) === wid .&&. pred (succ wid) === wid describe "Trivial" $ do it "WorkerIdIsZero" $ do scheduleWorkId trivialScheduler_ (`shouldBe` 0) withTrivialScheduler (`scheduleWorkId` pure) `shouldReturn` [0] it "TerminateDoesNothing" $ do terminate_ trivialScheduler_ `shouldReturn` () terminate trivialScheduler_ () `shouldReturn` () terminateWith trivialScheduler_ () `shouldReturn` () prop "TerminateSeq" $ prop_Terminate withTrivialScheduler terminate (\xs x -> xs ++ [x]) prop "TerminateWithSeq" $ prop_Terminate withTrivialScheduler terminateWith (\_ x -> [x]) prop "TerminateSeqR" $ prop_Terminate withTrivialSchedulerR terminate FinishedEarly prop "TerminateWithSeqR" $ prop_Terminate withTrivialSchedulerR terminateWith (const FinishedEarlyWith) describe "Seq" $ do prop "SameList" $ prop_SameList Seq prop "Recursive" $ prop_Recursive Seq prop "Nested" $ prop_Nested Seq prop "Serially" $ prop_Serially Seq prop "TrivialAsSeq_" prop_TrivialSchedulerSameAsSeq_ prop "replicateConcurrently == replicateM" $ prop_ReplicateM id . getSeqLike prop "replicateWork == replicateM" $ prop_ReplicateWork id . getSeqLike prop "replicateWork_ == replicateM" $ prop_ReplicateWork_ id . getSeqLike it "WorkerIdIsZero" $ withScheduler Seq (`scheduleWorkId` pure) `shouldReturn` [0] prop "TerminateSeq" $ prop_Terminate (withScheduler Seq) terminate (\xs x -> xs ++ [x]) prop "TerminateWithSeq" $ prop_Terminate (withScheduler Seq) terminateWith (\_ x -> [x]) prop "TerminateSeqR" $ prop_Terminate (withSchedulerR Seq) terminate FinishedEarly prop "TerminateWithSeqR" $ prop_Terminate (withSchedulerR Seq) terminateWith (const FinishedEarlyWith) describe "ParOn" $ do prop "SameList" $ \cs -> prop_SameList (ParOn cs) prop "Recursive" $ \cs -> prop_Recursive (ParOn cs) prop "Nested" $ \cs -> prop_Nested (ParOn cs) prop "Serially" $ \cs -> prop_Serially (ParOn cs) describe "Arbitrary Comp" $ do prop "Trivial" prop_SameAsTrivialScheduler prop "ArbitraryCompNested" prop_ArbitraryCompNested prop "AllJobsProcessed" prop_AllJobsProcessed prop "traverseConcurrently == traverse" prop_Traverse prop "replicateConcurrently == replicateM" $ prop_ReplicateM sort prop "replicateWork == replicateM" $ prop_ReplicateWork sort prop "replicateWork_ == replicateM" $ prop_ReplicateWork_ sort describe "Exceptions" $ do prop "CatchDivideByZero" prop_CatchDivideByZero prop "CatchDivideByZeroNested" prop_CatchDivideByZeroNested prop "KillBlockedCoworker" prop_KillBlockedCoworker prop "KillSleepingCoworker" prop_KillSleepingCoworker prop "ExpectAsyncException" prop_ExpectAsyncException prop "WorkerCaughtAsyncException" prop_WorkerCaughtAsyncException prop "AllWorkersDied" prop_AllWorkersDied prop "traverseConcurrently_" prop_TraverseConcurrently_ prop "traverseConcurrentlyInfinite_" prop_TraverseConcurrentlyInfinite_ describe "Premature" $ do prop "FinishEarly" prop_FinishEarly prop "FinishEarly_" prop_FinishEarly_ prop "FinishEarlyWith" prop_FinishEarlyWith prop "FinishBeforeStarting" prop_FinishBeforeStarting prop "FinishWithBeforeStarting" prop_FinishWithBeforeStarting describe "WorkerState" $ do prop "MutexException" prop_MutexException prop "WorkerStateExclusive" prop_WorkerStateExclusive describe "Restartable" $ do prop "ManyJobsInChunks" prop_ManyJobsInChunks prop "FindCancelResume" prop_FindCancelResume instance Arbitrary WorkerId where arbitrary = WorkerId <$> arbitrary instance Arbitrary a => Arbitrary (Results a) where arbitrary = oneof [ Finished <$> arbitrary , FinishedEarly <$> arbitrary <*> arbitrary , FinishedEarlyWith <$> arbitrary ] -- | Assert a synchronous exception assertExceptionIO :: (NFData a, Exception exc) => (exc -> Bool) -- ^ Return True if that is the exception that was expected -> IO a -- ^ IO Action that should throw an exception -> Property assertExceptionIO isExc action = monadicIO $ do hasFailed <- run $ catch (do res <- action res `deepseq` return False) $ \exc -> displayException exc `deepseq` return (isExc exc) assert hasFailed assertAsyncExceptionIO :: (Exception e, NFData a) => (e -> Bool) -> IO a -> Property assertAsyncExceptionIO isAsyncExc action = monadicIO $ do hasFailed <- run $ EUnsafe.catch (do res <- action res `deepseq` return False) (\exc -> case EUnsafe.asyncExceptionFromException exc of Just asyncExc | isAsyncExc asyncExc -> displayException asyncExc `deepseq` pure True _ -> EUnsafe.throwIO exc) assert hasFailed