-- Language extensions {{{ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DoRec #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE ViewPatterns #-} -- }}} -- Imports {{{ import Prelude hiding (catch) import Control.Applicative import Control.Arrow ((&&&),second) import Control.Concurrent import Control.Exception import Control.Lens (_1,_2,(%=),(<+=),use) import Control.Monad import Control.Monad.IO.Class import Control.Monad.Operational (ProgramViewT(..),view) import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.State (StateT,evalStateT) import Control.Monad.Trans.Writer import Data.Bits import Data.Composition ((.*)) import Data.Function import Data.Functor.Identity import qualified Data.IntSet as IntSet import Data.IntSet (IntSet) import Data.IORef import qualified Data.IVar as IVar import Data.List (sort) import Data.Maybe import qualified Data.Map as Map import Data.Monoid import Data.Sequence ((<|),(|>)) import qualified Data.Sequence as Seq import qualified Data.Serialize as Serialize import Data.Serialize (Serialize(),encode) import qualified Data.Set as Set import Data.Set (Set) import Data.Typeable import qualified Data.UUID as UUID import Data.UUID (UUID) import Data.Void (absurd) import Data.Word import Debug.Trace (trace) import Text.Printf import System.Directory (getTemporaryDirectory,removeFile) import System.IO import System.IO.Unsafe import System.Log.Logger (Priority(..),updateGlobalLogger,rootLoggerName,setLevel) import System.Random import Test.Framework import Test.Framework.Providers.HUnit import Test.Framework.Providers.QuickCheck2 import qualified Test.Framework.Providers.SmallCheck as Small import Test.HUnit hiding (Test,Path) import Test.QuickCheck.Arbitrary hiding ((><)) import Test.QuickCheck.Gen import Test.QuickCheck.Instances () import Test.QuickCheck.Modifiers import Test.QuickCheck.Monadic import Test.QuickCheck.Property hiding ((.&.),(==>)) import Test.SmallCheck ((==>)) import Test.SmallCheck.Series (Serial(..)) import Test.SmallCheck.Drivers as Small (test) import LogicGrowsOnTrees import LogicGrowsOnTrees.Checkpoint import LogicGrowsOnTrees.Examples.MapColoring import LogicGrowsOnTrees.Location import qualified LogicGrowsOnTrees.Parallel.Adapter.Threads as Threads import qualified LogicGrowsOnTrees.Parallel.Common.Workgroup as Workgroup import LogicGrowsOnTrees.Parallel.ExplorationMode import LogicGrowsOnTrees.Parallel.Main (RunOutcome(..),TerminationReason(..)) import LogicGrowsOnTrees.Parallel.Purity import LogicGrowsOnTrees.Path import LogicGrowsOnTrees.Parallel.Common.RequestQueue hiding (setWorkloadBufferSize) import LogicGrowsOnTrees.Parallel.Common.Supervisor import LogicGrowsOnTrees.Utils.Handle (send,receive) import LogicGrowsOnTrees.Utils.PerfectTree import LogicGrowsOnTrees.Utils.WordSum import LogicGrowsOnTrees.Workload import LogicGrowsOnTrees.Parallel.Common.Worker -- }}} -- Helpers {{{ -- Instances {{{ -- Newtypes {{{ newtype UniqueTreeT m = UniqueTree { unwrapUniqueTree :: TreeT m IntSet } newtype NullTreeT m = NullTree { unwrapNullTree :: TreeT m IntSet } -- }}} -- Arbitrary {{{ instance Arbitrary BranchChoice where arbitrary = elements [LeftBranch,RightBranch] instance Arbitrary UUID where -- {{{ arbitrary = MkGen (\r _ -> fst (random r)) -- }}} instance (Arbitrary α, Monoid α, Serialize α, Functor m, Monad m) ⇒ Arbitrary (TreeT m α) where -- {{{ arbitrary = fmap ($ mempty) (sized arb) where arb :: Monoid α ⇒ Int → Gen (α → TreeT m α) arb 0 = null arb 1 = frequency [(1,null) ,(1,processPlus) ,(2,resultPlus) ,(2,cachedPlus) ] arb n = frequency [(1,liftM2 (>=>) processPlus (arb n)) ,(2,liftM2 (>=>) resultPlus (arb n)) ,(2,liftM2 (>=>) cachedPlus (arb n)) ,(4, do left_size ← choose (0,n) let right_size = n-left_size liftM2 (liftA2 mplus) (arb left_size) (arb right_size) ) ] null :: Gen (α → TreeT m α) null = return (const mzero) result, cached :: Gen (TreeT m α) result = fmap return arbitrary cached = fmap cache arbitrary resultPlus, cachedPlus :: Monoid α ⇒ Gen (α → TreeT m α) resultPlus = (\x → flip fmap x . mappend) <$> result cachedPlus = (\x → flip fmap x . mappend) <$> cached processPlus :: Gen (α → TreeT m α) processPlus = return processPendingRequestsAndReturn -- }}} instance Monad m ⇒ Arbitrary (NullTreeT m) where -- {{{ arbitrary = (NullTree . ($ (const $ return ()))) <$> randomNullTreeWithHooks -- }}} instance Monad m ⇒ Arbitrary (UniqueTreeT m) where -- {{{ arbitrary = (UniqueTree . ($ (const $ return ()))) <$> randomUniqueTreeWithHooks -- }}} instance Arbitrary Checkpoint where -- {{{ arbitrary = sized arb where arb 0 = elements [Explored,Unexplored] arb n = frequency [(1,return Explored) ,(1,return Unexplored) ,(1,liftM2 CachePoint (fmap encode (arbitrary :: Gen Int)) (arb (n-1))) ,(2,liftM2 ChoicePoint (arb (n `div` 2)) (arb (n `div` 2))) ] -- }}} instance Arbitrary Location where arbitrary = fmap labelFromBranching (arbitrary :: Gen [BranchChoice]) instance Arbitrary Step where -- {{{ arbitrary = oneof [CacheStep <$> arbitrary ,ChoiceStep <$> arbitrary ] -- }}} instance Arbitrary α ⇒ Arbitrary (Solution α) where -- {{{ arbitrary = Solution <$> arbitrary <*> arbitrary -- }}} instance Arbitrary α ⇒ Arbitrary (Progress α) where -- {{{ arbitrary = Progress <$> arbitrary <*> arbitrary -- }}} instance Arbitrary α ⇒ Arbitrary (ProgressUpdate α) where -- {{{ arbitrary = ProgressUpdate <$> arbitrary <*> arbitrary -- }}} instance Arbitrary α ⇒ Arbitrary (StolenWorkload α) where -- {{{ arbitrary = StolenWorkload <$> arbitrary <*> arbitrary -- }}} instance Arbitrary Workload where -- {{{ arbitrary = Workload <$> arbitrary <*> arbitrary -- }}} -- }}} -- Serial {{{ instance Serial IO All where series = All <$> series instance Serial IO Any where series = Any <$> series instance Serial IO Word where series = (fromIntegral :: Int → Word) . abs <$> series instance Serial IO (Sum Int) where series = Sum <$> series instance Serial IO (Set String) where series = Set.fromList <$> series -- }}} -- Serialize {{{ instance Serialize UUID where -- {{{ put = Serialize.putLazyByteString . UUID.toByteString get = fromJust . UUID.fromByteString <$> Serialize.getLazyByteString 16 -- }}} instance Serialize (Sum Int) where -- {{{ put = Serialize.put . getSum get = fmap Sum Serialize.get -- }}} -- }}} Serialize -- Show {{{ instance Show UniqueTree where show = show . unwrapUniqueTree -- }}} -- }}} -- Exceptions {{{ -- TestException {{{ data TestException = TestException Int deriving (Eq,Show,Typeable) instance Exception TestException -- }}} -- }}} -- Type alises {{{ type UniqueTree = UniqueTreeT Identity type NullTree = NullTreeT Identity -- }}} -- Functions {{{ addAcceptOneWorkloadAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef (Maybe (worker_id,Workload)),SupervisorCallbacks exploration_mode worker_id IO) addAcceptOneWorkloadAction actions = do maybe_worker_and_workload_ref ← newIORef (Nothing :: Maybe (worker_id,Workload)) return (maybe_worker_and_workload_ref, actions { sendWorkloadToWorker = \workload worker_id → do maybe_old_workload ← readIORef maybe_worker_and_workload_ref case maybe_old_workload of Nothing → return () Just _ → error "workload has been submitted already!" writeIORef maybe_worker_and_workload_ref $ Just (worker_id,workload) }) -- }}} addAcceptMultipleWorkloadsAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef [(worker_id,Workload)],SupervisorCallbacks exploration_mode worker_id IO) addAcceptMultipleWorkloadsAction actions = do workers_and_workloads_ref ← newIORef [] return (workers_and_workloads_ref, actions { sendWorkloadToWorker = \workload worker_id → readIORef workers_and_workloads_ref >>= writeIORef workers_and_workloads_ref . (++ [(worker_id,workload)]) }) -- }}} addAppendWorkloadStealBroadcastIdsAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef [[worker_id]],SupervisorCallbacks exploration_mode worker_id IO) addAppendWorkloadStealBroadcastIdsAction actions = do broadcasts_ref ← newIORef ([] :: [[worker_id]]) return (broadcasts_ref, actions { broadcastWorkloadStealToWorkers = \worker_ids → modifyIORef broadcasts_ref (++ [worker_ids]) }) -- }}} addAppendProgressBroadcastIdsAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef [[worker_id]],SupervisorCallbacks exploration_mode worker_id IO) addAppendProgressBroadcastIdsAction actions = do broadcasts_ref ← newIORef ([] :: [[worker_id]]) return (broadcasts_ref, actions { broadcastProgressUpdateToWorkers = \worker_ids → modifyIORef broadcasts_ref (++ [worker_ids]) }) -- }}} addReceiveCurrentProgressAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef (Maybe (ProgressFor exploration_mode)),SupervisorCallbacks exploration_mode worker_id IO) addReceiveCurrentProgressAction actions = do maybe_progress_ref ← newIORef (Nothing :: Maybe ip) return (maybe_progress_ref, actions { receiveCurrentProgress = \progress → do maybe_old_progress ← readIORef maybe_progress_ref case maybe_old_progress of Nothing → return () Just _ → error "progress update has been received already!" writeIORef maybe_progress_ref $ Just progress }) -- }}} addSetWorkloadStealBroadcastIdsAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → IO (IORef [worker_id],SupervisorCallbacks exploration_mode worker_id IO) addSetWorkloadStealBroadcastIdsAction actions = do broadcasts_ref ← newIORef ([] :: [worker_id]) return (broadcasts_ref, actions { broadcastWorkloadStealToWorkers = writeIORef broadcasts_ref }) -- }}} checkFoundAgainstThreshold :: Int → IntSet → (IntSet,Bool) → IO Bool -- {{{ checkFoundAgainstThreshold threshold solutions (result,found) | found = do assertBool "check that the result set is big enough" $ IntSet.size result >= threshold assertBool "check that the results are all in the full set of solutions" $ result `IntSet.isSubsetOf` solutions return True | otherwise = do assertBool (printf "check that the unsuccessful result is small enough (%i < %i)" (IntSet.size result) threshold) $ IntSet.size result < threshold assertEqual "check that the result equals the solutions" solutions result return True -- }}} echo :: Show α ⇒ α → α -- {{{ echo x = trace (show x) x -- }}} echoWithLocation :: Show α ⇒ String → α → α -- {{{ echoWithLocation label x = trace (label ++ " " ++ show x) x -- }}} ignoreAcceptWorkloadAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → SupervisorCallbacks exploration_mode worker_id IO ignoreAcceptWorkloadAction actions = actions { sendWorkloadToWorker = \_ _ → return () } -- }}} ignoreWorkloadStealAction :: -- {{{ SupervisorCallbacks exploration_mode worker_id IO → SupervisorCallbacks exploration_mode worker_id IO ignoreWorkloadStealAction actions = actions { broadcastWorkloadStealToWorkers = \_ → return () } -- }}} frequencyT :: (MonadTrans t, Monad (t Gen)) ⇒ [(Int, t Gen a)] → t Gen a -- {{{ frequencyT [] = error "frequencyT used with empty list" frequencyT xs0 = lift (choose (1, tot)) >>= pick xs0 where tot = sum (map fst xs0) pick ((k,x):xs) n | n <= k = x | otherwise = pick xs (n-k) pick _ _ = error "frequencyT.pick used with empty list" -- }}} shuffle :: [α] → Gen [α] -- {{{ shuffle [] = return [] shuffle items = do index ← choose (0,length items-1) let hd = items !! index rest = take index items ++ drop (index+1) items tl ← shuffle rest return (hd:tl) -- }}} processPendingRequestsAndReturn :: Monad m ⇒ α → TreeT m α -- {{{ processPendingRequestsAndReturn x = processPendingRequests >> return x -- }}} randomCheckpointForTree :: Monoid α ⇒ Tree α → Gen (α,Checkpoint) -- {{{ randomCheckpointForTree (TreeT tree) = go1 tree where go1 tree = frequency [(1,return (exploreTree (TreeT tree),Explored)) ,(1,return (mempty,Unexplored)) ,(3,go2 tree) ] go2 (view → Cache (Identity (Just x)) :>>= k) = fmap (second $ CachePoint (encode x)) (go1 (k x)) go2 (view → Choice (TreeT x) (TreeT y) :>>= k) = liftM2 (\(left_result,left) (right_result,right) → (left_result `mappend` right_result, ChoicePoint left right) ) (go1 (x >>= k)) (go1 (y >>= k)) go2 (view → ProcessPendingRequests :>>= k) = go2 (k ()) go2 tree = elements [(exploreTree (TreeT tree),Explored),(mempty,Unexplored)] -- }}} randomNullTreeWithHooks :: ∀ m. Monad m ⇒ Gen ((Int → m ()) → TreeT m IntSet) -- {{{ randomNullTreeWithHooks = fmap (($ 0) . curry) . sized $ \n → evalStateT (arb1 n 0) (-1,IntSet.empty) where arb1, arb2 :: Int → Int → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet) arb1 n intermediate = do id ← _1 <+= 1 tree ← arb2 n intermediate return $ \args@(_,runHook) → lift (runHook id) >> tree args arb2 0 _ = return (const mzero) arb2 1 _ = return (const mzero) arb2 n intermediate = frequencyT [(1,generateForNext processPendingRequestsAndReturn intermediate (arb1 n)) ,(2,generateForNext return intermediate (arb1 n)) ,(2,generateForNext cache intermediate (arb1 n)) ,(4, do left_size ← lift $ choose (0,n) let right_size = n-left_size liftM2 (liftA2 mplus) (arb1 left_size intermediate) (arb1 right_size intermediate) ) ] generateForNext :: -- {{{ Monad m ⇒ (Int → TreeT m Int) → Int → (Int → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet)) → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet) generateForNext construct intermediate next = do x ← lift arbitrary let new_intermediate = x `xor` intermediate tree ← next new_intermediate return $ \(value,runHook) → do new_value ← construct . xor x $ value tree (new_value,runHook) -- }}} -- }}} randomPathForTree :: Tree α → Gen Path -- {{{ randomPathForTree (TreeT tree) = go tree where go (view → Cache (Identity (Just x)) :>>= k) = oneof [return Seq.empty ,fmap (CacheStep (encode x) <|) (go (k x)) ] go (view → Choice (TreeT x) (TreeT y) :>>= k) = oneof [return Seq.empty ,fmap (ChoiceStep LeftBranch <|) (go (x >>= k)) ,fmap (ChoiceStep RightBranch <|) (go (y >>= k)) ] go (view → ProcessPendingRequests :>>= k) = go (k ()) go _ = return Seq.empty -- }}} randomUniqueTreeWithHooks :: ∀ m. Monad m ⇒ Gen ((Int → m ()) → TreeT m IntSet) -- {{{ randomUniqueTreeWithHooks = fmap (($ 0) . curry) . sized $ \n → evalStateT (arb1 n 0) (-1,IntSet.empty) where arb1, arb2 :: Int → Int → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet) arb1 n intermediate = do id ← _1 <+= 1 tree ← arb2 n intermediate return $ \args@(_,runHook) → lift (runHook id) >> tree args arb2 0 _ = return (const mzero) arb2 1 intermediate = frequencyT [(1,return (const mzero)) ,(1,generateUnique processPendingRequestsAndReturn intermediate) ,(3,generateUnique return intermediate) ,(2,generateUnique cache intermediate) ] arb2 n intermediate = frequencyT [(1,generateForNext processPendingRequestsAndReturn intermediate (arb1 n)) ,(2,generateForNext return intermediate (arb1 n)) ,(2,generateForNext cache intermediate (arb1 n)) ,(4, do left_size ← lift $ choose (0,n) let right_size = n-left_size liftM2 (liftA2 mplus) (arb1 left_size intermediate) (arb1 right_size intermediate) ) ] generateUnique :: -- {{{ Monad m ⇒ (IntSet → TreeT m IntSet) → Int → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet) generateUnique construct intermediate = do observed ← use _2 x ← lift (arbitrary `suchThat` (flip IntSet.notMember observed . (xor intermediate))) let final_value = x `xor` intermediate _2 %= IntSet.insert final_value return $ construct . IntSet.singleton . xor x . fst -- }}} generateForNext :: -- {{{ Monad m ⇒ (Int → TreeT m Int) → Int → (Int → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet)) → StateT (Int,IntSet) Gen ((Int,Int → m ()) → TreeT m IntSet) generateForNext construct intermediate next = do x ← lift arbitrary let new_intermediate = x `xor` intermediate tree ← next new_intermediate return $ \(value,runHook) → do new_value ← construct . xor x $ value tree (new_value,runHook) -- }}} -- }}} randomTreeWithoutCache :: Arbitrary α ⇒ Gen (Tree α) -- {{{ randomTreeWithoutCache = sized arb where arb 0 = frequency [(2,result) ,(1,null) ] arb n = frequency [(2,result) ,(1,bindToArbitrary n result) ,(1,bindToArbitrary n null) ,(1,bindToArbitrary n process) ,(3,liftM2 mplus (arb (n `div` 2)) (arb (n `div` 2))) ] null = return mzero result = fmap return arbitrary process = fmap processPendingRequestsAndReturn arbitrary bindToArbitrary n = flip (liftM2 (>>)) (arb (n-1)) -- }}} remdups :: (Eq a) => [a] -> [a] -- {{{ remdups [] = [] remdups (x : []) = [x] remdups (x : xx : xs) | x == xx = remdups (x : xs) | otherwise = x : remdups (xx : xs) -- }}} -- }}} -- Values {{{ bad_test_supervisor_actions :: SupervisorCallbacks exploration_mode worker_id m -- {{{ bad_test_supervisor_actions = SupervisorCallbacks { broadcastProgressUpdateToWorkers = error "broadcastProgressUpdateToWorkers called! :-/" , broadcastWorkloadStealToWorkers = error "broadcastWorkloadStealToWorkers called! :-/" , receiveCurrentProgress = error "receiveCurrentProgress called! :-/" , sendWorkloadToWorker = error "sendWorkloadToWorker called! :-/" } -- }}} ignore_supervisor_actions :: Monad m ⇒ SupervisorCallbacks exploration_mode worker_id m -- {{{ ignore_supervisor_actions = SupervisorCallbacks { broadcastProgressUpdateToWorkers = const $ return () , broadcastWorkloadStealToWorkers = const $ return () , receiveCurrentProgress = const $ return () , sendWorkloadToWorker = const . const $ return () } -- }}} endless_tree = endless_tree `mplus` endless_tree -- }}} -- }}} main = do -- updateGlobalLogger rootLoggerName (setLevel DEBUG) defaultMain tests tests = -- {{{ [testGroup "test helpers" $ -- {{{ [testProperty "UniqueTree has unique results" $ \(UniqueTree tree) → -- {{{ let results = exploreTree (fmap (:[]) tree ) in length results == IntSet.size (mconcat results) -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees" -- {{{ [testGroup "Eq instance" -- {{{ [testProperty "self" $ \(v :: Tree [()]) → v == v ] -- }}} ,testProperty "allFrom" $ \(x :: [Int]) → x == allFrom x ,testProperty "between" $ do -- {{{ x ← choose ( 0,100) :: Gen Int y ← choose (50,100) return $ between x y == [x..y] -- }}} ,testGroup "exploreTree" -- {{{ [testCase "return" $ exploreTree (return [()]) @?= [()] ,testCase "mzero" $ exploreTree (mzero :: Tree [()]) @?= [] ,testCase "mplus" $ exploreTree (return [1::Int] `mplus` return [2]) @?= [1,2] ,testCase "cache" $ exploreTree (cache [42]) @?= [42::Int] ,testGroup "cacheMaybe" -- {{{ [testCase "Nothing" $ exploreTree (cacheMaybe (Nothing :: Maybe [()])) @?= [] ,testCase "Just" $ exploreTree (cacheMaybe (Just [42])) @?= [42::Int] ] -- }}} ,testGroup "cacheGuard" -- {{{ [testCase "True" $ exploreTree (cacheGuard False >> return [()]) @?= [] ,testCase "False" $ exploreTree (cacheGuard True >> return [()]) @?= [()] ] -- }}} ] -- }}} ,testGroup "exploreTreeT" -- {{{ [testCase "Writer" $ -- {{{ (runWriter . exploreTreeT $ do cache [1 :: Int] >>= lift . tell (lift (tell [2]) `mplus` lift (tell [3])) return [42::Int] ) @?= ([42,42],[1,2,3]) -- }}} ] -- }}} ,testGroup "exploreTreeTAndIgnoreResults" -- {{{ [testCase "Writer" $ -- {{{ (runWriter . exploreTreeTAndIgnoreResults $ do cache [1 :: Int] >>= lift . tell (lift (tell [2]) `mplus` lift (tell [3])) return [42::Int] ) @?= ((),[1,2,3]) -- }}} ] -- }}} ,testGroup "exploreTreeUntilFirst" -- {{{ [testCase "return" $ exploreTreeUntilFirst (return 42) @=? (Just 42 :: Maybe Int) ,testCase "null" $ exploreTreeUntilFirst mzero @=? (Nothing :: Maybe Int) ,testProperty "compared to exploreTree" $ \(tree :: Tree String) → exploreTreeUntilFirst tree == case exploreTree (fmap (:[]) tree) of [] → Nothing (x:_) → Just x ] -- }}} ,testGroup "exploreTreeTUntilFirst" -- {{{ [testCase "return" $ runIdentity (exploreTreeTUntilFirst (return 42)) @=? (Just 42 :: Maybe Int) ,testCase "null" $ runIdentity(exploreTreeTUntilFirst mzero) @=? (Nothing :: Maybe Int) ,testProperty "compared to exploreTreeT" $ \(tree :: TreeT Identity String) → runIdentity (exploreTreeTUntilFirst tree) == case runIdentity (exploreTreeT (fmap (:[]) tree)) of [] → Nothing (x:_) → Just x ] -- }}} ,testGroup "exploreTreeUntilFound" -- {{{ [testProperty "compared to exploreTree" $ do UniqueTree tree ← arbitrary let solutions = exploreTree tree threshold ← (+1) <$> choose (0,2*IntSet.size solutions) return . unsafePerformIO . checkFoundAgainstThreshold threshold solutions $ exploreTreeUntilFound ((>= threshold) . IntSet.size) tree ] -- }}} ,testGroup "exploreTreeTUntilFound" -- {{{ [testProperty "compared to exploreTreeT" $ do UniqueTree tree ← arbitrary let solutions = runIdentity (exploreTreeT tree) threshold ← (+1) <$> choose (0,2*IntSet.size solutions) return . unsafePerformIO . checkFoundAgainstThreshold threshold solutions . runIdentity $ exploreTreeTUntilFound ((>= threshold) . IntSet.size) tree ] -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Checkpoint" -- {{{ [testGroup "contextFromCheckpoint" -- {{{ [testProperty "cache" $ \(checkpoint :: Checkpoint) (i :: Int) → -- {{{ checkpointFromContext (Seq.singleton (CacheContextStep (encode i))) checkpoint == (simplifyCheckpointRoot $ CachePoint (encode i) checkpoint) -- }}} ,testProperty "left branch" $ \(inner_checkpoint :: Checkpoint) (other_tree :: Tree [()]) (other_checkpoint :: Checkpoint) → -- {{{ (checkpointFromContext (Seq.singleton (LeftBranchContextStep other_checkpoint other_tree)) inner_checkpoint) == (simplifyCheckpointRoot $ ChoicePoint inner_checkpoint other_checkpoint) -- }}} ,testProperty "right branch" $ \(checkpoint :: Checkpoint) → -- {{{ checkpointFromContext (Seq.singleton RightBranchContextStep) checkpoint == (simplifyCheckpointRoot $ ChoicePoint Explored checkpoint) -- }}} ,testProperty "empty" $ \(checkpoint :: Checkpoint) → -- {{{ checkpointFromContext Seq.empty checkpoint == checkpoint -- }}} ] -- }}} ,testProperty "invertCheckpoint" $ \(tree :: Tree (Set UUID)) → -- {{{ randomCheckpointForTree tree >>= \(partial_result,checkpoint) → return $ partial_result == exploreTreeStartingFromCheckpoint (invertCheckpoint checkpoint) tree -- }}} ,testGroup "Monoid instance" -- {{{ [testProperty "product results in intersection of solutions" $ \(UniqueTree tree) → do -- {{{ (_,checkpoint1) ← randomCheckpointForTree tree (_,checkpoint2) ← randomCheckpointForTree tree let checkpoint3 = checkpoint1 `mappend` checkpoint2 solutions1 = exploreTreeStartingFromCheckpoint checkpoint1 tree solutions2 = exploreTreeStartingFromCheckpoint checkpoint2 tree solutions3 = exploreTreeStartingFromCheckpoint checkpoint3 tree return $ solutions3 == solutions1 `IntSet.intersection` solutions2 -- }}} ,testCase "throws the correct exceptions" $ -- {{{ mapM_ (\(x,y) → try ( evaluate (x `mappend` y) >> assertFailure (show x ++ " and " ++ show y ++ " were not recognized as being inconsistent") ) >>= assertEqual "the thrown exception was incorrect" (Left $ InconsistentCheckpoints x y) ) [((CachePoint (encode (42 :: Int)) Unexplored),(CachePoint (encode (42 :: Integer)) Unexplored)) ,((ChoicePoint Unexplored Unexplored),CachePoint (encode (42 :: Int)) Unexplored) ] -- }}} ,testProperty "unit element laws" $ \(checkpoint :: Checkpoint) → -- {{{ (mempty `mappend` checkpoint == checkpoint) && (checkpoint `mappend` mempty == checkpoint) -- }}} ] -- }}} ,testProperty "stepThroughTreeStartingFromCheckpoint" $ do -- {{{ UniqueTree tree ← arbitrary (partial_result,checkpoint) ← randomCheckpointForTree tree let go state@ExplorationTState{..} current_result = exploreTreeStartingFromCheckpoint (invertCheckpoint (checkpointFromExplorationState state)) tree == current_result && case stepThroughTreeStartingFromCheckpoint state of (Just result,Nothing) → exploreTree tree == current_result <> result (Nothing,Nothing) → exploreTree tree == current_result (Just result,Just new_state) → go new_state (current_result <> result) (Nothing,Just new_state) → go new_state current_result return $ go (initialExplorationState checkpoint tree) partial_result -- }}} ,testProperty "exploreTreeStartingFromCheckpoint" $ \(UniqueTree tree) → -- {{{ randomCheckpointForTree tree >>= \(partial_result,checkpoint) → return $ exploreTree tree == mappend partial_result (exploreTreeStartingFromCheckpoint checkpoint tree) -- }}} ,testProperty "exploreTreeUntilFirstStartingFromCheckpoint" $ \(UniqueTree tree) → -- {{{ randomCheckpointForTree tree >>= \(_,checkpoint) → return $ let all_results = exploreTreeStartingFromCheckpoint checkpoint tree maybe_first_result = exploreTreeUntilFirstStartingFromCheckpoint checkpoint tree in case maybe_first_result of Nothing → IntSet.null all_results Just result → IntSet.size result == 1 && IntSet.member (IntSet.findMin result) all_results -- }}} ,testProperty "exploreTreeTUntilFirstStartingFromCheckpoint" $ \(UniqueTree tree) → -- {{{ randomCheckpointForTree tree >>= \(_,checkpoint) → return $ let all_results = exploreTreeStartingFromCheckpoint checkpoint tree maybe_first_result = runIdentity $ exploreTreeTUntilFirstStartingFromCheckpoint checkpoint tree in case maybe_first_result of Nothing → IntSet.null all_results Just result → IntSet.size result == 1 && IntSet.member (IntSet.findMin result) all_results -- }}} ,testProperty "exploreTreeUntilFoundStartingFromCheckpoint" $ do -- {{{ UniqueTree tree ← arbitrary (_,checkpoint) ← randomCheckpointForTree tree let solutions = exploreTreeStartingFromCheckpoint checkpoint tree threshold ← (+1) <$> choose (0,2*IntSet.size solutions) return . unsafePerformIO . checkFoundAgainstThreshold threshold solutions $ exploreTreeUntilFoundStartingFromCheckpoint ((>= threshold) . IntSet.size) checkpoint tree -- }}} ,testProperty "exploreTreeTUntilFoundStartingFromCheckpoint" $ do -- {{{ UniqueTree tree ← arbitrary (_,checkpoint) ← randomCheckpointForTree tree let solutions = exploreTreeStartingFromCheckpoint checkpoint tree threshold ← (+1) <$> choose (0,2*IntSet.size solutions) return . unsafePerformIO . checkFoundAgainstThreshold threshold solutions . runIdentity $ exploreTreeTUntilFoundStartingFromCheckpoint ((>= threshold) . IntSet.size) checkpoint tree -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Examples" -- {{{ [testProperty name $ do number_of_colors ← choose (2,5) number_of_countries ← choose (3,7) neighbor_probability ← choose (0,1::Float) neighbors ← fmap (concat . concat) $ forM [1..number_of_countries] $ \x → forM [x+1..number_of_countries] $ \y → do outcome ← choose (0,1) return $ if outcome > neighbor_probability then [(x,y),(y,x)] else [] let solutions = computeSolutions number_of_colors number_of_countries (\x y → (x,y) `elem` neighbors) morallyDubiousIOProperty $ do forM_ solutions $ \solution → forM_ solution $ \(country_1,color_1) → forM_ solution $ \(country_2,color_2) → when ((country_1,country_2) `elem` neighbors) $ assertBool "neighbors have different colors" $ color_1 /= color_2 let correct_count = sum $ do solution ← zip [1..] <$> replicateM (fromIntegral number_of_countries) [1..number_of_colors] forM_ solution $ \(country_1,color_1) → forM_ solution $ \(country_2,color_2) → when ((country_1,country_2) `elem` neighbors) $ guard $ color_1 /= color_2 return 1 computeCount number_of_colors solutions @?= correct_count return True | (name,computeSolutions,computeCount) ← [("coloringSolutions",coloringSolutions,curry (fromIntegral . length . snd)) ,("coloringUniqueSolutions",coloringUniqueSolutions, \number_of_colors → sum . map (\solution → let number_of_colors_used = maximum . fmap snd $ solution in product [number_of_colors-number_of_colors_used+1..number_of_colors] ) ) ] ] -- }}} ,testGroup "LogicGrowsOnTrees.Location" -- {{{ [testProperty "branchingFromLocation . labelFromBranching = id" $ -- {{{ liftA2 (==) (branchingFromLocation . labelFromBranching) id -- }}} ,testProperty "labelFromBranching . branchingFromLocation = id" $ -- {{{ liftA2 (==) (labelFromBranching . branchingFromLocation) id -- }}} ,testGroup "Monoid instance" -- {{{ [testProperty "equivalent to concatenation of branchings" $ \(parent_branching :: [BranchChoice]) (child_branching :: [BranchChoice]) → -- {{{ labelFromBranching parent_branching `mappend` labelFromBranching child_branching == labelFromBranching (parent_branching `mappend` child_branching) -- }}} ,testProperty "obeys monoid laws" $ -- {{{ liftA2 (&&) (liftA2 (==) id (`mappend` (mempty :: Location))) (liftA2 (==) id ((mempty :: Location) `mappend`)) -- }}} ] -- }}} ,testProperty "Ord instance of Location equivalent to Ord of branching" $ \a b → -- {{{ (compare `on` branchingFromLocation) a b == compare a b -- }}} ,testGroup "exploreTreeWithLocations" -- {{{ [testProperty "same result as exploreTree" $ \(tree :: Tree [()]) → exploreTree ((:[]) <$> tree) == (solutionResult <$> exploreTreeWithLocations tree) ] -- }}} ,testGroup "sendTreeDownLocation" -- {{{ [testProperty "same result as walking down path" $ do -- {{{ tree :: Tree Int ← randomTreeWithoutCache path ← randomPathForTree tree let label = labelFromPath path return $ sendTreeDownPath path tree == sendTreeDownLocation label tree -- }}} ] -- }}} ,testProperty "exploreLocatableTree" $ -- {{{ let gen _ 0 = return mzero gen label 1 = return (All . (== label) <$> getLocation) gen label n = do left_size ← choose (0,n) let right_size = n-left_size left ← gen (leftBranchOf label) left_size right ← gen (rightBranchOf label) right_size return $ left `mplus` right in getAll . exploreLocatableTree <$> sized (gen rootLocation) -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Parallel.Adapter.Threads" $ -- {{{ [testGroup "FirstMode" -- {{{ [testCase "two threads, one blocked" $ do -- {{{ RunOutcome _ termination_reason ← Threads.exploreTreeIOUntilFirst (Workgroup.setNumberOfWorkers 2) (liftIO (threadDelay 1) >> endless_tree `mplus` return () ) termination_reason @?= Completed (Just (Progress (ChoicePoint Unexplored Explored) ())) -- }}} ] -- }}} ,testGroup "FoundModeUsingPull" -- {{{ [testCase "many threads with combined final result but none finish" $ do -- {{{ RunOutcome _ termination_reason ← Threads.exploreTreeIOUntilFoundUsingPull ((== 2) . length) (Workgroup.setNumberOfWorkers 4) ((return [1] `mplus` endless_tree) `mplus` (return [2] `mplus` endless_tree)) case termination_reason of Completed (Right (Progress _ result)) → sort result @?= [1,2] _ → fail $ "got incorrect result: " ++ show termination_reason -- }}} ] -- }}} ,testGroup "FoundModeUsingPush" -- {{{ [testCase "two threads with combined final result but none finish" $ do -- {{{ RunOutcome _ termination_reason ← Threads.exploreTreeIOUntilFoundUsingPush ((== 2) . length) (Workgroup.setNumberOfWorkers 2) ((return [1] `mplus` endless_tree) `mplus` (return [2] `mplus` endless_tree)) case termination_reason of Completed (Right (Progress _ result)) → sort result @?= [1,2] _ → fail $ "got incorrect result: " ++ show termination_reason -- }}} ] -- }}} ,plusTestOptions (mempty {topt_maximum_generated_tests = Just 10}) $ testGroup "stress tests" $ -- {{{ let extractResult (RunOutcome _ termination_reason) = -- {{{ case termination_reason of Aborted _ → error "prematurely aborted" Completed result → return result Failure _ message → error message -- }}} insertHooks cleared_flags_mvar request_queue = ($ \id → liftIO $ do -- {{{ threadDelay 10 mvar ← modifyMVar cleared_flags_mvar $ \cleared_flags → case Map.lookup id cleared_flags of Nothing → do mvar ← newEmptyMVar writeChan request_queue mvar return (Map.insert id mvar cleared_flags,mvar) Just mvar → return (cleared_flags,mvar) readMVar mvar ) -- }}} receiveProgressInto progresses_ref progress = atomicModifyIORef progresses_ref ((progress:) &&& const ()) respondToRequests request_queue generateNoise progresses_ref = do -- {{{ Workgroup.setNumberOfWorkers 1 forever $ do liftIO $ threadDelay 10 mvar ← liftIO $ readChan request_queue liftIO $ threadDelay 10 generateNoise $ receiveProgressInto progresses_ref liftIO $ threadDelay 10 liftIO $ putMVar mvar () -- }}} oneThreadNoise receiveProgress = liftIO (randomRIO (0,1::Int)) >>= \i → case i of -- {{{ 0 → do Workgroup.setNumberOfWorkers 0 Workgroup.setNumberOfWorkers 1 1 → void $ requestProgressUpdateAsync receiveProgress -- }}} twoThreadsNoise receiveProgress = liftIO (randomRIO (0,1::Int)) >>= \i → case i of -- {{{ 0 → void $ Workgroup.changeNumberOfWorkers (3-) 1 → void $ requestProgressUpdateAsync receiveProgress -- }}} manyThreadsNoise receiveProgress = liftIO (randomRIO (0,2::Int)) >>= \i → case i of -- {{{ 0 → void $ Workgroup.changeNumberOfWorkers (\i → if i > 1 then i-1 else i) 1 → void $ Workgroup.changeNumberOfWorkers (+1) 2 → void $ requestProgressUpdateAsync receiveProgress -- }}} in [testGroup "AllMode" $ -- {{{ let runTest generateNoise = randomUniqueTreeWithHooks >>= \constructTree → morallyDubiousIOProperty $ do cleared_flags_mvar ← newMVar mempty request_queue ← newChan progresses_ref ← newIORef [] result ← (Threads.exploreTreeIO (respondToRequests request_queue generateNoise progresses_ref) (insertHooks cleared_flags_mvar request_queue constructTree) ) >>= extractResult let tree = constructTree (const $ return ()) correct_result ← exploreTreeT tree result @?= correct_result (remdups <$> readIORef progresses_ref) >>= mapM_ (\(Progress checkpoint result) → do exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= (@?= result) exploreTreeTStartingFromCheckpoint checkpoint tree >>= (@?= correct_result ) . mappend result ) return True in [testProperty "one thread" . runTest $ oneThreadNoise ,testProperty "two threads" . runTest $ twoThreadsNoise ,testProperty "many threads" . runTest $ manyThreadsNoise ] -- }}} ,testGroup "FirstMode" $ -- {{{ let runTest generator generateNoise = generator >>= \constructTree → morallyDubiousIOProperty $ do cleared_flags_mvar ← newMVar mempty request_queue ← newChan progresses_ref ← newIORef [] maybe_result ← (Threads.exploreTreeIOUntilFirst (respondToRequests request_queue generateNoise progresses_ref) (insertHooks cleared_flags_mvar request_queue constructTree) ) >>= extractResult let tree = constructTree (const $ return ()) correct_results ← exploreTreeT tree case maybe_result of Nothing → assertBool "solutions were missed" (IntSet.null correct_results) Just (Progress checkpoint result) → do IntSet.size result @?= 1 assertBool "solution was not valid" $ result `IntSet.isSubsetOf` correct_results exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= (@=? result) exploreTreeTStartingFromCheckpoint checkpoint tree >>= (@=? IntSet.difference correct_results result) (remdups <$> readIORef progresses_ref) >>= mapM_ (\checkpoint → do exploreTreeTUntilFirstStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= (@?= Nothing) ) return True testGroupUsingGenerator name generator = testGroup name $ [testProperty "one thread" . runTest generator $ oneThreadNoise ,testProperty "two threads" . runTest generator $ twoThreadsNoise ,testProperty "many threads" . runTest generator $ manyThreadsNoise ] in [testGroupUsingGenerator "with solutions" randomUniqueTreeWithHooks ,testGroupUsingGenerator "without solutions" randomNullTreeWithHooks ] -- }}} ,testGroup "FoundModeUsingPull" $ -- {{{ let runTest generator generateNoise = generator >>= \constructTree → morallyDubiousIOProperty $ do let tree = constructTree (const $ return ()) all_results ← exploreTreeT tree number_of_results_to_find ← randomRIO (1,2*IntSet.size all_results) cleared_flags_mvar ← newMVar mempty request_queue ← newChan progresses_ref ← newIORef [] result ← (Threads.exploreTreeIOUntilFoundUsingPull ((>= number_of_results_to_find) . IntSet.size) (respondToRequests request_queue generateNoise progresses_ref) (insertHooks cleared_flags_mvar request_queue constructTree) ) >>= extractResult case result of Left incomplete_result → do assertBool "result is not smaller than desired" $ IntSet.size incomplete_result < number_of_results_to_find assertEqual "incomplete result matches all results" incomplete_result all_results Right (Progress checkpoint final_result) → do assertBool "final result is at least as large as desired" $ IntSet.size final_result >= number_of_results_to_find assertBool "final result was not valid" $ final_result `IntSet.isSubsetOf` all_results exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= assertEqual "final results together do not match all results covered by the checkpoint" final_result exploreTreeTStartingFromCheckpoint checkpoint tree >>= assertEqual "all results minus final results do not match remaining results" (IntSet.difference all_results final_result) (remdups <$> readIORef progresses_ref) >>= mapM_ (\(Progress checkpoint result) → do exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= (@?= result) exploreTreeTStartingFromCheckpoint checkpoint tree >>= (@?= all_results) . mappend result ) return True testGroupUsingGenerator name generator = testGroup name $ [testProperty "one thread" . runTest generator $ oneThreadNoise ,testProperty "two threads" . runTest generator $ twoThreadsNoise ,testProperty "many threads" . runTest generator $ manyThreadsNoise ] in [testGroupUsingGenerator "with solutions" randomUniqueTreeWithHooks ,testGroupUsingGenerator "without solutions" randomNullTreeWithHooks ] -- }}} ,testGroup "FoundModeUsingPush" $ -- {{{ let runTest generator generateNoise = generator >>= \constructTree → morallyDubiousIOProperty $ do let tree = constructTree (const $ return ()) all_results ← exploreTreeT tree number_of_results_to_find ← randomRIO (1,2*IntSet.size all_results) cleared_flags_mvar ← newMVar mempty request_queue ← newChan progresses_ref ← newIORef [] result ← (Threads.exploreTreeIOUntilFoundUsingPush ((>= number_of_results_to_find) . IntSet.size) (respondToRequests request_queue generateNoise progresses_ref) (insertHooks cleared_flags_mvar request_queue constructTree) ) >>= extractResult case result of Left incomplete_result → do assertBool "result is not smaller than desired" $ IntSet.size incomplete_result < number_of_results_to_find assertEqual "incomplete result matches all results" incomplete_result all_results Right (Progress checkpoint final_result) → do assertBool "result is at least as large as desired" $ IntSet.size final_result >= number_of_results_to_find assertBool "final result was not valid" $ final_result `IntSet.isSubsetOf` all_results exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= assertEqual "both returned results together do not match all results covered by the checkpoint" final_result exploreTreeTStartingFromCheckpoint checkpoint tree >>= assertEqual "all results minus return results do not match remaining results" (IntSet.difference all_results final_result) (remdups <$> readIORef progresses_ref) >>= mapM_ (\(Progress checkpoint result) → do exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree >>= (@?= result) exploreTreeTStartingFromCheckpoint checkpoint tree >>= (@?= all_results) . mappend result ) return True testGroupUsingGenerator name generator = testGroup name $ [testProperty "one thread" . runTest generator $ oneThreadNoise ,testProperty "two threads" . runTest generator $ twoThreadsNoise ,testProperty "many threads" . runTest generator $ manyThreadsNoise ] in [testGroupUsingGenerator "with solutions" randomUniqueTreeWithHooks ,testGroupUsingGenerator "without solutions" randomNullTreeWithHooks ] -- }}} ] -- }}} ,testCase "processPendingRequests" $ do mvar ← newEmptyMVar RunOutcome{..} ← Threads.exploreTreeIO (Threads.setNumberOfWorkers 2) $ let go = processPendingRequests >> liftIO (tryTakeMVar mvar) >>= maybe go return in go `mplus` liftIO (putMVar mvar ()) case runTerminationReason of Aborted _ → error "aborted" Completed () → return () Failure _ message → error message return () ] -- }}} ,testGroup "LogicGrowsOnTrees.Parallel.Common.RequestQueue" -- {{{ [testCase "kills all controller threads" $ do -- {{{ starts@[a,b,c,d] ← replicateM 4 newEmptyMVar vars@[w,x,y,z] ← replicateM 4 newEmptyMVar request_queue ← newRequestQueue forkControllerThread request_queue . liftIO $ do try (putMVar a () >> forever yield) >>= putMVar w forkControllerThread request_queue $ do fork (do fork . liftIO $ try (putMVar b () >> forever yield) >>= putMVar x liftIO $ try (putMVar c () >> forever yield) >>= putMVar y :: RequestQueueReader (AllMode ()) () IO () ) liftIO $ try (putMVar d () >> forever yield) >>= putMVar z forM_ starts $ takeMVar killControllerThreads request_queue forM_ vars $ \var → do value ← takeMVar var case value of Right () → assertFailure "Thread did not infinitely loop." Left e → case fromException e of Just ThreadKilled → return () _ → assertFailure $ "Unexpected exception: " ++ show e -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Parallel.Common.Supervisor" -- {{{ [testCase "immediately abort" $ do -- {{{ SupervisorOutcome{..} ← runSupervisor AllMode bad_test_supervisor_actions (UnrestrictedProgram abortSupervisor) supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= ([] :: [Int]) -- }}} ,testCase "failure" $ do -- {{{ SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode bad_test_supervisor_actions (receiveWorkerFailure () "FAIL" :: ∀ α. SupervisorMonad (AllMode ()) () IO α) supervisorTerminationReason @?= SupervisorFailure mempty () "FAIL" supervisorRemainingWorkers @?= [] -- }}} ,testGroup "adding and removing workers" -- {{{ [testGroup "without workload buffer" -- {{{ [testCase "add one worker then abort" $ do -- {{{ (maybe_workload_ref,actions) ← addAcceptOneWorkloadAction bad_test_supervisor_actions SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker () abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [()] readIORef maybe_workload_ref >>= (@?= Just ((),entire_workload)) -- }}} ,testCase "add then remove one worker then abort" $ do -- {{{ (maybe_workload_ref,actions) ← addAcceptOneWorkloadAction bad_test_supervisor_actions SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker () removeWorker () abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [] readIORef maybe_workload_ref >>= (@?= Just ((),entire_workload)) -- }}} ,testCase "add then remove then add one worker then abort" $ do -- {{{ (maybe_workload_ref,actions) ← addAcceptMultipleWorkloadsAction bad_test_supervisor_actions SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker 1 removeWorker 1 addWorker 2 abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [2::Int] readIORef maybe_workload_ref >>= (@?= [(1,entire_workload),(2,entire_workload)]) -- }}} ,testCase "add two workers then remove first worker then abort" $ do -- {{{ (maybe_workload_ref,actions1) ← addAcceptMultipleWorkloadsAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions2) ← addAppendWorkloadStealBroadcastIdsAction actions1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions2 $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker 1 addWorker 2 removeWorker 1 abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [2::Int] readIORef maybe_workload_ref >>= (@?= [(1,entire_workload),(2,entire_workload)]) readIORef broadcast_ids_list_ref >>= (@?= [[1]]) -- }}} ,testProperty "add then remove many workers then abort" $ do -- {{{ (NonEmpty worker_ids_to_add :: NonEmptyList UUID) ← arbitrary worker_ids_to_remove ← (fmap concat $ forM (tail worker_ids_to_add) $ \worker_id → do should_remove ← arbitrary if should_remove then return [worker_id] else return [] ) >>= shuffle let worker_ids_left = Set.toAscList $ Set.fromList worker_ids_to_add `Set.difference` Set.fromList worker_ids_to_remove monadicIO . run $ do (maybe_workload_ref,actions_1) ← addAcceptOneWorkloadAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions_2) ← addAppendWorkloadStealBroadcastIdsAction actions_1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions_2 $ do enableSupervisorDebugMode setWorkloadBufferSize 0 mapM_ addWorker worker_ids_to_add mapM_ removeWorker worker_ids_to_remove abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) sort supervisorRemainingWorkers @?= worker_ids_left readIORef maybe_workload_ref >>= (@?= Just (head worker_ids_to_add,entire_workload)) readIORef broadcast_ids_list_ref >>= (@?= if (null . tail) worker_ids_to_add then [] else [[head worker_ids_to_add]]) -- }}} ] -- }}} ,testGroup "with workload buffer" -- {{{ [testCase "add one worker then abort" $ do -- {{{ (maybe_workload_ref,actions1) ← addAcceptOneWorkloadAction bad_test_supervisor_actions (broadcasts_ref,actions2) ← addAppendWorkloadStealBroadcastIdsAction actions1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions2 $ do enableSupervisorDebugMode addWorker () abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [()] readIORef maybe_workload_ref >>= (@?= Just ((),entire_workload)) readIORef broadcasts_ref >>= (@?= [[()]]) -- }}} ,testCase "add then remove one worker then abort" $ do -- {{{ (maybe_workload_ref,actions1) ← addAcceptOneWorkloadAction bad_test_supervisor_actions (broadcasts_ref,actions2) ← addAppendWorkloadStealBroadcastIdsAction actions1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions2 $ do enableSupervisorDebugMode addWorker () removeWorker () abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [] readIORef maybe_workload_ref >>= (@?= Just ((),entire_workload)) readIORef broadcasts_ref >>= (@?= [[()]]) -- }}} ,testCase "add then remove then add one worker then abort" $ do -- {{{ (maybe_workload_ref,actions1) ← addAcceptMultipleWorkloadsAction bad_test_supervisor_actions (broadcasts_ref,actions2) ← addAppendWorkloadStealBroadcastIdsAction actions1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions2 $ do enableSupervisorDebugMode addWorker 1 removeWorker 1 addWorker 2 abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [2::Int] readIORef maybe_workload_ref >>= (@?= [(1,entire_workload),(2,entire_workload)]) readIORef broadcasts_ref >>= (@?= [[1],[2]]) -- }}} ] -- }}} ] -- }}} ,testGroup "progress updates" -- {{{ [testCase "request progress update when no workers present" $ do -- {{{ (maybe_progress_ref,actions) ← addReceiveCurrentProgressAction bad_test_supervisor_actions SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 performGlobalProgressUpdate abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= ([] :: [()]) readIORef maybe_progress_ref >>= (@?= Just (Progress Unexplored ())) -- }}} ,testProperty "request progress update when all active workers present leave" $ do -- {{{ number_of_active_workers ← choose (1,10 :: Int) number_of_inactive_workers ← choose (0,10) let active_workers = [0..number_of_active_workers-1] inactive_workers = [101..101+number_of_inactive_workers-1] monadicIO . run $ do (maybe_progress_ref,actions1) ← addReceiveCurrentProgressAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions2) ← addAppendProgressBroadcastIdsAction actions1 (workload_steal_ids_ref,actions3) ← addSetWorkloadStealBroadcastIdsAction actions2 let actions4 = ignoreAcceptWorkloadAction $ actions3 let progress = Progress Unexplored (Sum 0) SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions4 $ do setWorkloadBufferSize 0 addWorker 0 forM_ (zip [0..] (tail active_workers)) $ \(prefix_count,worker_id) → do addWorker worker_id [worker_to_steal_from] ← liftIO $ readIORef workload_steal_ids_ref let remaining_workload = Workload (Seq.replicate (prefix_count+1) (ChoiceStep LeftBranch)) Unexplored let stolen_workload = Workload (Seq.replicate (prefix_count) (ChoiceStep LeftBranch) |> (ChoiceStep RightBranch)) Unexplored receiveStolenWorkload worker_to_steal_from $ Just (StolenWorkload (ProgressUpdate mempty remaining_workload) stolen_workload) mapM_ addWorker inactive_workers performGlobalProgressUpdate mapM_ removeWorker active_workers abortSupervisor supervisorTerminationReason @?= SupervisorAborted progress supervisorRemainingWorkers @?= inactive_workers readIORef broadcast_ids_list_ref >>= (@?= [active_workers]) readIORef maybe_progress_ref >>= (@?= Just progress) -- }}} ,testCase "request and receive Just progress update when one worker present" $ do -- {{{ (maybe_progress_ref,actions1) ← addReceiveCurrentProgressAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions2) ← addAppendProgressBroadcastIdsAction actions1 let actions3 = ignoreAcceptWorkloadAction actions2 let progress = Progress (ChoicePoint Unexplored Unexplored) (Sum 1) SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions3 $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker () performGlobalProgressUpdate receiveProgressUpdate () $ ProgressUpdate progress entire_workload abortSupervisor supervisorTerminationReason @?= SupervisorAborted progress supervisorRemainingWorkers @?= [()] readIORef maybe_progress_ref >>= (@?= Just progress) readIORef broadcast_ids_list_ref >>= (@?= [[()]]) -- }}} ,testCase "request and receive progress update when active and inactive workers present" $ do -- {{{ (maybe_progress_ref,actions1) ← addReceiveCurrentProgressAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions2) ← addAppendProgressBroadcastIdsAction actions1 let actions3 = ignoreAcceptWorkloadAction . ignoreWorkloadStealAction $ actions2 let progress = Progress (ChoicePoint Unexplored Unexplored) (Sum 1) SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions3 $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker (1 :: Int) addWorker (2 :: Int) performGlobalProgressUpdate receiveProgressUpdate 1 $ ProgressUpdate progress entire_workload abortSupervisor supervisorTerminationReason @?= SupervisorAborted progress supervisorRemainingWorkers @?= [1,2] readIORef maybe_progress_ref >>= (@?= Just progress) readIORef broadcast_ids_list_ref >>= (@?= [[1]]) -- }}} ] -- }}} ,testGroup "workload steals" -- {{{ [testCase "failure to steal from a worker leads to second attempt" $ do -- {{{ (broadcast_ids_list_ref,actions1) ← addAppendWorkloadStealBroadcastIdsAction bad_test_supervisor_actions let actions2 = ignoreAcceptWorkloadAction actions1 SupervisorOutcome{..} ← runUnrestrictedSupervisor AllMode actions2 $ do addWorker (1::Int) addWorker 2 receiveStolenWorkload 1 Nothing abortSupervisor supervisorTerminationReason @?= SupervisorAborted (Progress Unexplored ()) supervisorRemainingWorkers @?= [1,2] readIORef broadcast_ids_list_ref >>= (@?= [[1],[1]]) -- }}} ] -- }}} ,testCase "starting from previous checkpoint" $ do -- {{{ (maybe_workload_ref,actions1) ← addAcceptOneWorkloadAction bad_test_supervisor_actions (broadcast_ids_list_ref,actions2) ← addAppendWorkloadStealBroadcastIdsAction actions1 let checkpoint = ChoicePoint Unexplored Unexplored progress = Progress checkpoint (Sum 1) SupervisorOutcome{..} ← runUnrestrictedSupervisorStartingFrom AllMode progress actions2 $ do addWorker () abortSupervisor supervisorTerminationReason @?= SupervisorAborted progress supervisorRemainingWorkers @?= [()] readIORef maybe_workload_ref >>= (@?= Just ((),(Workload Seq.empty checkpoint))) readIORef broadcast_ids_list_ref >>= (@?= [[()]]) -- }}} ,testGroup "FirstMode" $ -- {{{ [testGroup "single worker" -- {{{ [testCase "finishes with Explored" $ do -- {{{ SupervisorOutcome{..} ← runUnrestrictedSupervisor FirstMode ignore_supervisor_actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker () receiveWorkerFinished () (Progress Explored Nothing) error "Supervisor did not terminate" supervisorTerminationReason @?= SupervisorCompleted (Nothing :: Maybe (Progress ())) -- }}} ,testCase "finishes with result" $ do -- {{{ SupervisorOutcome{..} ← runUnrestrictedSupervisor FirstMode ignore_supervisor_actions $ do enableSupervisorDebugMode setWorkloadBufferSize 0 addWorker () receiveWorkerFinished () (Progress Explored (Just ())) error "Supervisor did not terminate" supervisorTerminationReason @?= SupervisorCompleted (Just (Progress Explored ())) -- }}} ] -- }}} ,testGroup "two workers" -- {{{ [testCase "both finish with Explored" $ do -- {{{ SupervisorOutcome{..} ← runUnrestrictedSupervisor FirstMode ignore_supervisor_actions $ do enableSupervisorDebugMode addWorker True addWorker False receiveStolenWorkload True . Just $ StolenWorkload (ProgressUpdate Unexplored (Workload (Seq.singleton $ ChoiceStep LeftBranch) Unexplored ) ) (Workload (Seq.singleton $ ChoiceStep RightBranch) Unexplored ) receiveWorkerFinished True (Progress (ChoicePoint Explored Unexplored) Nothing) receiveWorkerFinished False (Progress (ChoicePoint Unexplored Explored) Nothing) error "Supervisor did not terminate" supervisorTerminationReason @?= SupervisorCompleted (Nothing :: Maybe (Progress ())) -- }}} ,testCase "both finish with result" $ do -- {{{ SupervisorOutcome{..} ← runUnrestrictedSupervisor FirstMode ignore_supervisor_actions $ do enableSupervisorDebugMode addWorker True addWorker False receiveStolenWorkload True . Just $ StolenWorkload (ProgressUpdate Unexplored (Workload (Seq.singleton $ ChoiceStep LeftBranch) Unexplored ) ) (Workload (Seq.singleton $ ChoiceStep RightBranch) Unexplored ) receiveWorkerFinished False (Progress (ChoicePoint Explored Unexplored) (Just False)) receiveWorkerFinished True (Progress (ChoicePoint Unexplored Explored) (Just True)) error "Supervisor did not terminate" supervisorTerminationReason @?= SupervisorCompleted (Just (Progress (ChoicePoint Explored Unexplored) False)) -- }}} ] -- }}} ] -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Parallel.Common.Worker" -- {{{ [testGroup "forkWorkerThread" -- {{{ [testCase "abort" $ do -- {{{ termination_result_ivar ← IVar.new semaphore ← newEmptyMVar WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) (liftIO (takeMVar semaphore) `mplus` error "should never get here") entire_workload absurd sendAbortRequest workerPendingRequests putMVar semaphore () termination_result ← IVar.blocking $ IVar.read termination_result_ivar case termination_result of WorkerFinished _ → assertFailure "worker faled to abort" WorkerFailed exception → assertFailure ("worker threw exception: " ++ show exception) WorkerAborted → return () workerInitialPath @?= Seq.empty (IVar.nonblocking . IVar.read) workerTerminationFlag >>= assertBool "is the termination flag set?" . isJust -- }}} ,testGroup "obtains all solutions" -- {{{ [testProperty "with no initial path" $ \(tree :: Tree [Int]) → unsafePerformIO $ do -- {{{ solutions_ivar ← IVar.new _ ← forkWorkerThread AllMode Pure (IVar.write solutions_ivar) tree entire_workload absurd Progress checkpoint solutions ← (IVar.blocking $ IVar.read solutions_ivar) >>= \termination_reason → case termination_reason of WorkerFinished final_progress → return final_progress other → error ("terminated unsuccessfully with reason " ++ show other) checkpoint @?= Explored solutions @?= exploreTree tree return True -- }}} ,testProperty "with an initial path" $ \(tree :: Tree [Int]) → randomPathForTree tree >>= \path → return . unsafePerformIO $ do -- {{{ solutions_ivar ← IVar.new _ ← forkWorkerThread AllMode Pure (IVar.write solutions_ivar) tree (Workload path Unexplored) absurd Progress checkpoint solutions ← (IVar.blocking $ IVar.read solutions_ivar) >>= \termination_reason → case termination_reason of WorkerFinished final_progress → return final_progress other → error ("terminated unsuccessfully with reason " ++ show other) checkpoint @?= checkpointFromInitialPath path Explored solutions @?= (exploreTree . sendTreeDownPath path $ tree) return True -- }}} ] -- }}} ,testGroup "progress updates correctly capture current and remaining progress" $ -- {{{ let runAnalysis tree termination_flag termination_result_ivar progress_updates_ref = do -- {{{ termination_result ← IVar.blocking $ IVar.read termination_result_ivar remaining_solutions ← case termination_result of WorkerFinished (progressResult → solutions) → return solutions WorkerFailed exception → error ("worker threw exception: " ++ show exception) WorkerAborted → error "worker aborted prematurely" (IVar.nonblocking . IVar.read) termination_flag >>= assertBool "is the termination flag set?" . isJust progress_updates ← reverse <$> readIORef progress_updates_ref let correct_solutions = exploreTree tree update_solutions = map (progressResult . progressUpdateProgress) progress_updates all_solutions = remaining_solutions:update_solutions forM_ (zip [0..] all_solutions) $ \(i,solutions_1) → forM_ (zip [0..] all_solutions) $ \(j,solutions_2) → unless (i == j) $ assertBool "Is there an overlap between non-intersecting solutions?" (IntSet.null $ solutions_1 `IntSet.intersection` solutions_2) let total_solutions = mconcat all_solutions assertEqual "Are the total solutions correct?" correct_solutions total_solutions let accumulated_update_solutions = scanl1 mappend update_solutions sequence_ $ zipWith (\accumulated_solutions (ProgressUpdate (Progress checkpoint _) remaining_workload) → do let remaining_solutions = exploreTreeWithinWorkload remaining_workload tree assertBool "Is there overlap between the accumulated solutions and the remaining solutions?" (IntSet.null $ accumulated_solutions `IntSet.intersection` remaining_solutions) assertEqual "Do the accumulated and remaining solutions sum to the correct solutions?" correct_solutions (accumulated_solutions `mappend` remaining_solutions) assertEqual "Is the checkpoint equal to the the remaining solutions?" remaining_solutions (exploreTreeStartingFromCheckpoint checkpoint tree) assertEqual "Is the inverted checkpoint equal to the the accumulated solutions?" accumulated_solutions (exploreTreeStartingFromCheckpoint (invertCheckpoint checkpoint) tree) ) accumulated_update_solutions progress_updates return True in -- }}} [testProperty "continuous progress update requests" $ \(UniqueTree tree) → unsafePerformIO $ do -- {{{ starting_flag ← IVar.new termination_result_ivar ← IVar.new WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) ((liftIO . IVar.blocking . IVar.read $ starting_flag) >> endowTree tree) entire_workload absurd progress_updates_ref ← newIORef [] let sendMyProgressUpdateRequest = sendProgressUpdateRequest workerPendingRequests submitProgressUpdate submitProgressUpdate progress_update = do atomicModifyIORef progress_updates_ref ((progress_update:) &&& const ()) sendMyProgressUpdateRequest sendMyProgressUpdateRequest IVar.write starting_flag () runAnalysis tree workerTerminationFlag termination_result_ivar progress_updates_ref -- }}} ,testProperty "progress update requests at random leaves" $ \(UniqueTree tree) → unsafePerformIO $ do -- {{{ termination_result_ivar ← IVar.new progress_updates_ref ← newIORef [] rec WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) (do value ← endowTree tree liftIO $ randomIO >>= flip when submitMyProgressUpdateRequest return value ) entire_workload absurd let submitMyProgressUpdateRequest = sendProgressUpdateRequest workerPendingRequests (atomicModifyIORef progress_updates_ref . (&&& const ()) . (:)) runAnalysis tree workerTerminationFlag termination_result_ivar progress_updates_ref -- }}} ] -- }}} ,testCase "terminates successfully with null tree" $ do -- {{{ termination_result_ivar ← IVar.new WorkerEnvironment{..} ← forkWorkerThread AllMode Pure (IVar.write termination_result_ivar) (mzero :: Tree [Int]) entire_workload absurd termination_result ← IVar.blocking $ IVar.read termination_result_ivar case termination_result of WorkerFinished (progressResult → solutions) → solutions @?= mempty WorkerFailed exception → assertFailure ("worker threw exception: " ++ show exception) WorkerAborted → assertFailure "worker prematurely aborted" workerInitialPath @?= Seq.empty (IVar.nonblocking . IVar.read) workerTerminationFlag >>= assertBool "is the termination flag set?" . isJust -- }}} ,testGroup "work stealing correctly preserves total workload" $ -- {{{ let runManyStealsAnalysis tree termination_flag termination_result_ivar steals_ref = do -- {{{ termination_result ← IVar.blocking $ IVar.read termination_result_ivar (Progress _ remaining_solutions) ← case termination_result of WorkerFinished final_progress → return final_progress WorkerFailed exception → error ("worker threw exception: " ++ show exception) WorkerAborted → error "worker aborted prematurely" (IVar.nonblocking . IVar.read) termination_flag >>= assertBool "is the termination flag set?" . isJust steals ← reverse <$> readIORef steals_ref let correct_solutions = exploreTree tree prestolen_solutions = map ( progressResult . progressUpdateProgress . stolenWorkloadProgressUpdate ) steals stolen_solutions = map ( flip exploreTreeWithinWorkload tree . stolenWorkload ) steals all_solutions = remaining_solutions:(prestolen_solutions ++ stolen_solutions) total_solutions = mconcat all_solutions forM_ (zip [0..] all_solutions) $ \(i,solutions_1) → forM_ (zip [0..] all_solutions) $ \(j,solutions_2) → unless (i == j) $ assertBool "Is there overlap between non-intersecting solutions?" (IntSet.null $ solutions_1 `IntSet.intersection` solutions_2) assertEqual "Do the steals together include all of the solutions?" correct_solutions total_solutions let accumulated_prestolen_solutions = scanl1 mappend prestolen_solutions accumulated_stolen_solutions = scanl1 mappend stolen_solutions sequence_ $ zipWith3 (\acc_prestolen acc_stolen (StolenWorkload (ProgressUpdate (Progress checkpoint _) remaining_workload) _) → do let remaining_solutions = exploreTreeWithinWorkload remaining_workload tree accumulated_solutions = acc_prestolen `mappend` acc_stolen assertBool "Is there overlap between the accumulated solutions and the remaining solutions?" (IntSet.null $ accumulated_solutions `IntSet.intersection` remaining_solutions) assertEqual "Do the accumulated and remaining solutions sum to the correct solutions?" correct_solutions (accumulated_solutions `mappend` remaining_solutions) assertEqual "Is the checkpoint equal to the stolen plus the remaining solutions?" (acc_stolen `mappend` remaining_solutions) (exploreTreeStartingFromCheckpoint checkpoint tree) ) accumulated_prestolen_solutions accumulated_stolen_solutions steals return True in -- }}} [testProperty "single steal" $ \(UniqueTree tree :: UniqueTree) → unsafePerformIO $ do -- {{{ reached_position_mvar ← newEmptyMVar blocking_value_ivar ← IVar.new let tree_with_blocking_value = mplus (mplus (liftIO $ do _ ← tryPutMVar reached_position_mvar () IVar.blocking . IVar.read $ blocking_value_ivar ) (return (IntSet.singleton 101010101)) ) (endowTree tree) termination_result_ivar ← IVar.new WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) tree_with_blocking_value entire_workload absurd maybe_workload_ref ← newIORef Nothing takeMVar reached_position_mvar sendWorkloadStealRequest workerPendingRequests $ writeIORef maybe_workload_ref IVar.write blocking_value_ivar (IntSet.singleton 202020202) Progress _ remaining_solutions ← (IVar.blocking $ IVar.read termination_result_ivar) >>= \termination_result → case termination_result of WorkerFinished final_progress → return final_progress WorkerFailed exception → error ("worker threw exception: " ++ show exception) WorkerAborted → error "worker aborted prematurely" (IVar.nonblocking . IVar.read) workerTerminationFlag >>= assertBool "is the termination flag set?" . isJust StolenWorkload (ProgressUpdate (Progress checkpoint prestolen_solutions) remaining_workload) stolen_workload ← fmap (fromMaybe (error "stolen workload not available")) $ readIORef maybe_workload_ref assertBool "Does the checkpoint have unexplored nodes?" $ simplifyCheckpointRoot checkpoint /= Explored exploreTreeTWithinWorkload remaining_workload tree_with_blocking_value >>= (remaining_solutions @?=) exploreTreeTStartingFromCheckpoint (invertCheckpoint checkpoint) tree_with_blocking_value >>= (prestolen_solutions @?=) correct_solutions ← exploreTreeT tree_with_blocking_value stolen_solutions ← exploreTreeTWithinWorkload stolen_workload tree_with_blocking_value correct_solutions @=? mconcat [prestolen_solutions,remaining_solutions,stolen_solutions] assertEqual "There is no overlap between the prestolen solutions and the remaining solutions." IntSet.empty (prestolen_solutions `IntSet.intersection` remaining_solutions) assertEqual "There is no overlap between the prestolen solutions and the stolen solutions." IntSet.empty (prestolen_solutions `IntSet.intersection` stolen_solutions) assertEqual "There is no overlap between the stolen solutions and the remaining solutions." IntSet.empty (stolen_solutions `IntSet.intersection` remaining_solutions) return True -- }}} ,testProperty "continuous stealing" $ \(UniqueTree tree) → unsafePerformIO $ do -- {{{ starting_flag ← IVar.new termination_result_ivar ← IVar.new WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) ((liftIO . IVar.blocking . IVar.read $ starting_flag) >> endowTree tree) entire_workload absurd steals_ref ← newIORef [] let submitMyWorkloadStealRequest = sendWorkloadStealRequest workerPendingRequests submitStolenWorkload submitStolenWorkload Nothing = submitMyWorkloadStealRequest submitStolenWorkload (Just steal) = do atomicModifyIORef steals_ref ((steal:) &&& const ()) submitMyWorkloadStealRequest submitMyWorkloadStealRequest IVar.write starting_flag () runManyStealsAnalysis tree workerTerminationFlag termination_result_ivar steals_ref -- }}} ,testProperty "stealing at random leaves" $ \(UniqueTree tree) → unsafePerformIO $ do -- {{{ termination_result_ivar ← IVar.new steals_ref ← newIORef [] rec WorkerEnvironment{..} ← forkWorkerThread AllMode io_purity (IVar.write termination_result_ivar) (do value ← endowTree tree liftIO $ randomIO >>= flip when submitMyWorkloadStealRequest return value ) entire_workload absurd let submitMyWorkloadStealRequest = sendWorkloadStealRequest workerPendingRequests (maybe (return ()) $ atomicModifyIORef steals_ref . (&&& const ()) . (:)) runManyStealsAnalysis tree workerTerminationFlag termination_result_ivar steals_ref -- }}} ] -- }}} ] -- }}} ,testProperty "exploreTreeUntilFirst" $ \(tree :: Tree String) → morallyDubiousIOProperty $ do -- {{{ termination_reason ← exploreTreeGeneric FirstMode Pure tree case termination_reason of WorkerFinished maybe_final_progress → return $ (progressResult <$> maybe_final_progress) == exploreTreeUntilFirst tree _ → fail $ "returned " ++ show termination_reason ++ " instead of WorkerFinished" -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Path" -- {{{ [testGroup "sendTreeDownPath" -- {{{ [testCase "null path" $ (exploreTree . sendTreeDownPath Seq.empty) (return [42]) @?= [42] ,testCase "cache" $ do (exploreTree . sendTreeDownPath (Seq.singleton (CacheStep (encode ([42 :: Int]))))) (cache (undefined :: [Int])) @?= [42] ,testCase "cacheGuard" $ do (exploreTree . sendTreeDownPath (Seq.singleton (CacheStep (encode ())))) (cacheGuard False >> return [42::Int]) @?= [42] ,testCase "choice" $ do -- {{{ (exploreTree . sendTreeDownPath (Seq.singleton (ChoiceStep LeftBranch))) (return [42] `mplus` undefined) @?= [42] (exploreTree . sendTreeDownPath (Seq.singleton (ChoiceStep RightBranch))) (undefined `mplus` return [42]) @?= [42] -- }}} ,testGroup "errors" -- {{{ [testGroup "PastTreeIsInconsistentWithPresentTree" -- {{{ [testCase "cache step with choice" $ -- {{{ try ( evaluate . exploreTree $ sendTreeDownPath (Seq.singleton (CacheStep undefined :: Step)) (undefined `mplus` undefined :: Tree [Int]) ) >>= (@?= Left PastTreeIsInconsistentWithPresentTree) -- }}} ,testCase "choice step with cache" $ -- {{{ try ( evaluate . exploreTree $ sendTreeDownPath (Seq.singleton (ChoiceStep undefined :: Step)) (cache undefined :: Tree [Int]) ) >>= (@?= Left PastTreeIsInconsistentWithPresentTree) -- }}} ] -- }}} ,testGroup "TreeEndedBeforeEndOfWalk" -- {{{ [testCase "mzero" $ -- {{{ try ( evaluate . exploreTree $ sendTreeDownPath (Seq.singleton (undefined :: Step)) (mzero :: Tree [Int]) ) >>= (@?= Left TreeEndedBeforeEndOfWalk) -- }}} ,testCase "return" $ -- {{{ try ( evaluate . exploreTree $ sendTreeDownPath (Seq.singleton (undefined :: Step)) (return (undefined :: [Int])) ) >>= (@?= Left TreeEndedBeforeEndOfWalk) -- }}} ] -- }}} ] -- }}} ] -- }}} ,testGroup "walkThroughTreeT" -- {{{ [testCase "cache step" $ do -- {{{ let (transformed_tree,log) = runWriter . sendTreeTDownPath (Seq.singleton (CacheStep . encode $ [24 :: Int])) $ do runAndCache (tell [1] >> return [42 :: Int] :: Writer [Int] [Int]) log @?= [] (runWriter . exploreTreeT $ transformed_tree) @?= ([24],[]) -- }}} ,testCase "choice step" $ do -- {{{ let (transformed_tree,log) = runWriter . sendTreeTDownPath (Seq.singleton (ChoiceStep RightBranch)) $ do lift (tell [1]) (lift (tell [2]) `mplus` lift (tell [3])) lift (tell [4]) return [42] log @?= [1] (runWriter . exploreTreeT $ transformed_tree) @?= ([42],[3,4]) -- }}} ] -- }}} ] -- }}} ,testGroup "LogicGrowsOnTrees.Utils.PerfectTree" -- {{{ [Small.testProperty "trivialPerfectTree" . Small.test $ -- {{{ (liftA2 . liftA2) (==>) (\arity _ → arity >= 2) ((liftA2 . liftA2) (==) numberOfLeaves ((getWordSum . exploreTree) .* trivialPerfectTree) ) -- }}} ] -- }}} ,testProperty "LogicGrowsOnTrees.Utils.Handle" $ \(x::UUID) → morallyDubiousIOProperty $ bracket (getTemporaryDirectory >>= flip openBinaryTempFile "test-handles") (\(filepath,handle) → do hClose handle removeFile filepath ) (\(_,handle) → do send handle x hSeek handle AbsoluteSeek 0 y ← receive handle return (x == y) ) ] -- }}}