{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} module Spec.DynamicLogic.RegistryModel where import Control.Concurrent import Control.Exception (ErrorCall, try) import Data.List import Test.QuickCheck import Test.QuickCheck.Monadic import Test.Tasty hiding (after) import Test.Tasty.QuickCheck (testProperty) import Data.Data (Typeable) import Spec.DynamicLogic.Registry import Test.QuickCheck.DynamicLogic import Test.QuickCheck.StateModel data RegState = RegState { tids :: [Var ThreadId] , regs :: [(String, Var ThreadId)] , dead :: [Var ThreadId] } deriving (Show) deriving instance Show (Action RegState a) deriving instance Eq (Action RegState a) instance StateModel RegState where data Action RegState a where Spawn :: Action RegState ThreadId WhereIs :: String -> Action RegState (Maybe ThreadId) Register :: String -> Var ThreadId -> Action RegState (Either ErrorCall ()) Unregister :: String -> Action RegState (Either ErrorCall ()) KillThread :: Var ThreadId -> Action RegState () -- not generated Successful :: Typeable a => Action RegState a -> Action RegState a type ActionMonad RegState = IO arbitraryAction s = frequency [ (max 1 $ 10 - length (tids s), return $ Some Spawn) , ( max 1 $ 10 - length (regs s) , Some <$> ( Register <$> probablyUnregistered s <*> elements (tids s) ) ) , ( 2 * length (regs s) , Some <$> ( Unregister <$> probablyRegistered s ) ) , ( 10 , Some <$> ( WhereIs <$> probablyRegistered s ) ) , (max 1 $ 3 - length (dead s), Some <$> (KillThread <$> elements (tids s))) ] shrinkAction s (Register name tid) = [Some (Unregister name)] ++ [Some (Register name' tid) | name' <- shrinkName name] ++ [Some (Register name tid') | tid' <- shrinkTid s tid] shrinkAction _ (Unregister name) = [Some (WhereIs name)] ++ [Some (Unregister name') | name' <- shrinkName name] shrinkAction _ (WhereIs name) = [Some (WhereIs name') | name' <- shrinkName name] shrinkAction _ Spawn = [] shrinkAction s (KillThread tid) = [Some (KillThread tid') | tid' <- shrinkTid s tid] shrinkAction s (Successful act) = Some act : [Some (Successful act') | Some act' <- shrinkAction s act] initialState :: RegState initialState = RegState [] [] [] nextState s Spawn step = s{tids = step : tids s} nextState s (Register name tid) _step | positive s (Register name tid) = s{regs = (name, tid) : regs s} | otherwise = s nextState s (Unregister name) _step = s{regs = filter ((/= name) . fst) (regs s)} nextState s (KillThread tid) _ = s{dead = tid : dead s, regs = filter ((/= tid) . snd) (regs s)} nextState s (Successful act) step = nextState s act step nextState s _ _ = s precondition s (Register _name step) = step `elem` tids s -- && positive s (Register name step) precondition s (KillThread tid) = tid `elem` tids s precondition s (Successful act) = precondition s act precondition _ _ = True postcondition s (WhereIs name) env mtid = (env <$> lookup name (regs s)) == mtid postcondition _s Spawn _ _ = True postcondition s (Register name step) _ res = positive s (Register name step) == (res == Right ()) postcondition _s (Unregister _name) _ _ = True postcondition _s (KillThread _) _ _ = True postcondition _s (Successful (Register _ _)) _ res = res == Right () postcondition s (Successful act) env res = postcondition s act env res monitoring (_s, s') act _ res = counterexample ("\nState: " ++ show s' ++ "\n") . tabulate "Registry size" [show $ length (regs s')] . case act of Register _ _ -> tabu "Register" [ case res of Left _ -> "fails " ++ why _s act Right () -> "succeeds" ] Unregister _ -> tabu "Unregister" [case res of Left _ -> "fails"; Right () -> "succeeds"] WhereIs _ -> tabu "WhereIs" [case res of Nothing -> "fails"; Just _ -> "succeeds"] _ -> id perform _ Spawn _ = forkIO (threadDelay 10000000) perform _ (Register name tid) env = try $ register name (env tid) perform _ (Unregister name) _ = try $ unregister name perform _ (WhereIs name) _ = whereis name perform _ (KillThread tid) env = do killThread (env tid) threadDelay 1000 perform s (Successful act) env = perform s act env positive :: RegState -> Action RegState a -> Bool positive s (Register name tid) = name `notElem` map fst (regs s) && tid `notElem` map snd (regs s) && tid `notElem` dead s positive s (Unregister name) = name `elem` map fst (regs s) positive _s _ = True instance DynLogicModel RegState where restricted (Successful _) = True restricted _ = False why :: RegState -> Action RegState a -> String why s (Register name tid) = unwords $ ["name already registered" | name `elem` map fst (regs s)] ++ ["tid already registered" | tid `elem` map snd (regs s)] ++ ["dead thread" | tid `elem` dead s] why _ _ = "(impossible)" arbitraryName :: Gen String arbitraryName = elements allNames probablyRegistered :: RegState -> Gen String probablyRegistered s = oneof $ map (return . fst) (regs s) ++ [arbitraryName] probablyUnregistered :: RegState -> Gen String probablyUnregistered s = elements $ allNames ++ (allNames \\ map fst (regs s)) shrinkName :: String -> [String] shrinkName name = [n | n <- allNames, n < name] allNames :: [String] allNames = ["a", "b", "c", "d", "e"] shrinkTid :: RegState -> Var ThreadId -> [Var ThreadId] shrinkTid s tid = [tid' | tid' <- tids s, tid' < tid] tabu :: String -> [String] -> Property -> Property tabu tab xs = tabulate tab xs . foldr (.) id [classify True (tab ++ ": " ++ x) | x <- xs] prop_Registry :: Actions RegState -> Property prop_Registry s = monadicIO $ do _ <- run cleanUp monitor $ counterexample "\nExecution\n" _ <- runActions s assert True cleanUp :: IO [Either ErrorCall ()] cleanUp = sequence [ try (unregister name) :: IO (Either ErrorCall ()) | name <- allNames ++ ["x"] ] propTest :: DynFormula RegState -> Property propTest d = forAllScripts d prop_Registry -- Generate normal test cases normalTests :: s -> DynFormula s normalTests _ = passTest ||| afterAny normalTests loopingTests :: s -> DynFormula s loopingTests _ = afterAny loopingTests canSpawn :: RegState -> DynFormula RegState canSpawn _ = after Spawn done canRegisterA :: RegState -> DynFormula RegState canRegisterA s | null (tids s) = after Spawn canRegisterA | otherwise = after (Successful $ Register "a" (head (tids s))) done -- test that the registry never contains more than k processes regLimit :: Int -> RegState -> DynFormula RegState regLimit k s | length (regs s) > k = ignore -- fail? yes, gets stuck at this point | otherwise = passTest ||| afterAny (regLimit k) -- test that we can register a pid that is not dead, if we unregister the name first. canRegisterUndead :: RegState -> DynFormula RegState canRegisterUndead s | null aliveTs = ignore | otherwise = after (Successful (Register "x" (head aliveTs))) done where aliveTs = tids s \\ dead s canRegister :: RegState -> DynFormula RegState canRegister s | length (regs s) == 5 = ignore -- all names are in use | null (tids s) = after Spawn canRegister | otherwise = forAllQ ( elementsQ (allNames \\ map fst (regs s)) , elementsQ (tids s) ) $ \(name, tid) -> after (Successful $ Register name tid) done canRegisterName :: String -> RegState -> DynFormula RegState canRegisterName name s = forAllQ (elementsQ availableTids) $ \tid -> after (Successful $ Register name tid) done where availableTids = tids s \\ map snd (regs s) canReregister :: RegState -> DynFormula RegState canReregister s | null (regs s) = ignore | otherwise = forAllQ (elementsQ $ map fst (regs s)) $ \name -> after (Unregister name) (canRegisterName name) canRegisterName' :: String -> RegState -> DynFormula RegState canRegisterName' name s = forAllQ (elementsQ availableTids) $ \tid -> after (Successful $ Register name tid) done where availableTids = (tids s \\ map snd (regs s)) \\ dead s canReregister' :: RegState -> DynFormula RegState canReregister' s | null (regs s) = toStop $ if null availableTids then after Spawn canReregister' else after (Register "a" (head availableTids)) canReregister' | otherwise = forAllQ (elementsQ $ map fst (regs s)) $ \name -> after (Unregister name) (canRegisterName' name) where availableTids = (tids s \\ map snd (regs s)) \\ dead s tests :: TestTree tests = testGroup "registry model example" [testProperty "prop_Registry" prop_Registry]