module Test.Ganeti.Locking.Waiting (testLocking_Waiting) where
import Control.Applicative (liftA2)
import Control.Monad (liftM)
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Text.JSON as J
import Test.QuickCheck
import Test.Ganeti.TestCommon
import Test.Ganeti.TestHelper
import Test.Ganeti.Locking.Allocation (TestLock, TestOwner, requestSucceeded)
import Ganeti.BasicTypes (isBad, genericResult, runListHead)
import Ganeti.Locking.Allocation (LockRequest, listLocks)
import qualified Ganeti.Locking.Allocation as L
import Ganeti.Locking.Types (Lock)
import Ganeti.Locking.Waiting
data UpdateRequest a b c = Update b [LockRequest a]
| UpdateWaiting c b [LockRequest a]
| RemovePending b
| IntersectRequest b [a]
| OpportunisticUnion b [(a, L.OwnerState)]
deriving Show
instance (Arbitrary a, Arbitrary b, Arbitrary c)
=> Arbitrary (UpdateRequest a b c) where
arbitrary =
frequency [ (2, Update <$> arbitrary <*> (choose (1, 4) >>= vector))
, (4, UpdateWaiting <$> arbitrary <*> arbitrary
<*> (choose (1, 4) >>= vector))
, (1, RemovePending <$> arbitrary)
, (1, IntersectRequest <$> arbitrary
<*> (choose (1, 4) >>= vector))
, (1, OpportunisticUnion <$> arbitrary
<*> (choose (1, 4) >>= vector))
]
asWaitingTrans :: (Lock a, Ord b, Ord c)
=> LockWaiting a b c -> UpdateRequest a b c -> LockWaiting a b c
asWaitingTrans state (Update owner req) = fst $ updateLocks owner req state
asWaitingTrans state (UpdateWaiting prio owner req) =
fst $ updateLocksWaiting prio owner req state
asWaitingTrans state (RemovePending owner) = removePendingRequest owner state
asWaitingTrans state (IntersectRequest owner locks) =
fst $ intersectLocks locks owner state
asWaitingTrans state (OpportunisticUnion owner locks) =
fst $ opportunisticLockUnion owner locks state
foldUpdates :: (Lock a, Ord b, Ord c)
=> [UpdateRequest a b c] -> LockWaiting a b c
foldUpdates = foldl asWaitingTrans emptyWaiting
instance (Arbitrary a, Lock a, Arbitrary b, Ord b, Arbitrary c, Ord c)
=> Arbitrary (LockWaiting a b c) where
arbitrary = foldUpdates <$> (choose (0, 8) >>= vector)
prop_NoActionWithPendingRequests :: Property
prop_NoActionWithPendingRequests =
forAll (arbitrary :: Gen TestOwner) $ \a ->
forAll ((arbitrary :: Gen (LockWaiting TestLock TestOwner Integer))
`suchThat` (S.member a . getPendingOwners)) $ \state ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
forAll arbitrary $ \prio ->
counterexample "Owners with pending requests may not update locks"
. all (isBad . fst . snd)
$ [updateLocks, updateLocksWaiting prio] <*> [a] <*> [req] <*> [state]
forAllBlocked :: (Testable prop)
=> (LockWaiting TestLock TestOwner Integer
-> TestOwner
-> Integer
-> [LockRequest TestLock]
-> prop)
-> Property
forAllBlocked predicate =
forAll (arbitrary :: Gen TestOwner) $ \a ->
forAll (arbitrary :: Gen Integer) $ \prio ->
forAll (arbitrary `suchThat` (/=) a) $ \b ->
forAll ((arbitrary :: Gen (LockWaiting TestLock TestOwner Integer))
`suchThat` foldl (liftA2 (&&)) (const True)
[ not . S.member a . getPendingOwners
, M.null . listLocks a . getAllocation
, not . M.null . listLocks b . getAllocation]) $ \state ->
forAll ((arbitrary :: Gen [LockRequest TestLock])
`suchThat` (genericResult (const False) (not . S.null)
. fst . snd . flip (updateLocksWaiting prio a) state))
$ \req ->
predicate state a prio req
prop_WaitingRequestsGetPending :: Property
prop_WaitingRequestsGetPending =
forAllBlocked $ \state owner prio req ->
counterexample "After a not immediately fulfilled waiting request, owner\
\ must have a pending request"
. S.member owner . getPendingOwners . fst
$ updateLocksWaiting prio owner req state
prop_PendingGetFulfilledEventually :: Property
prop_PendingGetFulfilledEventually =
forAllBlocked $ \state owner prio req ->
let oldpending = getPendingOwners state
(state', (resultBlockers, _)) = updateLocksWaiting prio owner req state
blockers = genericResult (const S.empty) id resultBlockers
state'' = S.foldl (\s a -> fst $ releaseResources a s) state'
$ S.union oldpending blockers
finallyOwned = listLocks owner $ getAllocation state''
in counterexample "After all blockers and old pending owners give up their\
\ resources, a pending request must be granted\
\ automatically"
$ all (requestSucceeded finallyOwned) req
prop_PendingGetNotifiedEventually :: Property
prop_PendingGetNotifiedEventually =
forAllBlocked $ \state owner prio req ->
let oldpending = getPendingOwners state
(state', (resultBlockers, _)) = updateLocksWaiting prio owner req state
blockers = genericResult (const S.empty) id resultBlockers
releaseOneOwner (s, tonotify) o =
let (s', newnotify) = releaseResources o s
in (s', newnotify `S.union` tonotify)
(_, notified) = S.foldl releaseOneOwner (state', S.empty)
$ S.union oldpending blockers
in counterexample "After all blockers and old pending owners give up their\
\ resources, a pending owner must be notified"
$ S.member owner notified
prop_Progress :: Property
prop_Progress =
forAllBlocked $ \state owner prio req ->
let (state', (resultBlockers, _)) = updateLocksWaiting prio owner req state
blockers = genericResult (const S.empty) id resultBlockers
releaseOneOwner (s, tonotify) o =
let (s', newnotify) = releaseResources o s
in (s', newnotify `S.union` tonotify)
(_, notified) = S.foldl releaseOneOwner (state', S.empty) blockers
in counterexample "Some progress must be made after all blockers release\
\ their locks"
. not . S.null $ notified S.\\ blockers
prop_ProgressSound :: Property
prop_ProgressSound =
forAllBlocked $ \state owner prio req ->
let (state', (resultBlockers, _)) = updateLocksWaiting prio owner req state
blockers = genericResult (const S.empty) id resultBlockers
releaseOneOwner (s, tonotify) o =
let (s', newnotify) = releaseResources o s
in (s', newnotify `S.union` tonotify)
(state'', notified) = S.foldl releaseOneOwner (state', S.empty) blockers
requestFulfilled o =
runListHead False
(\(_, _, r) ->
all (requestSucceeded . listLocks o $ getAllocation state'') r)
. S.toList . S.filter (\(_, b, _) -> b == o)
. getPendingRequests $ state'
in counterexample "If an owner gets notified, his request must be satisfied"
. all requestFulfilled . S.toList $ notified S.\\ blockers
prop_PendingJustified :: Property
prop_PendingJustified =
forAll ((arbitrary :: Gen (LockWaiting TestLock TestOwner Integer))
`suchThat` (not . S.null . getPendingRequests)) $ \state ->
let isJustified (_, b, req) =
genericResult (const False) (not . S.null) . snd
. L.updateLocks b req $ getAllocation state
in counterexample "Pending requests must be good and not fulfillable"
. all isJustified . S.toList $ getPendingRequests state
prop_UpdateIdempotent :: Property
prop_UpdateIdempotent =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \owner ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
let (state', (answer', _)) = updateLocks owner req state
(state'', (answer'', nfy)) = updateLocks owner req state'
in conjoin [ counterexample ("repeated updateLocks waiting gave different\
\ answers: " ++ show answer' ++ " /= "
++ show answer'') $ answer' == answer''
, counterexample "updateLocks not idempotent"
$ extRepr state' == extRepr state''
, counterexample ("notifications (" ++ show nfy ++ ") on replay")
$ S.null nfy
]
prop_extReprPreserved :: Property
prop_extReprPreserved =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
let rep = extRepr state
rep' = extRepr $ fromExtRepr rep
in counterexample "a lock waiting obtained from an extensional representation\
\ must have the same extensional representation"
$ rep' == rep
prop_SimulateUpdateLocks :: Property
prop_SimulateUpdateLocks =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \owner ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
let state' = fromExtRepr $ extRepr state
(finState, (result, notify)) = updateLocks owner req state
(finState', (result', notify')) = updateLocks owner req state'
in counterexample "extRepr-equal states must behave equal on updateLocks"
$ and [ result == result'
, notify == notify'
, extRepr finState == extRepr finState'
]
prop_SimulateUpdateLocksWaiting :: Property
prop_SimulateUpdateLocksWaiting =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \owner ->
forAll (arbitrary :: Gen Integer) $ \prio ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
let state' = fromExtRepr $ extRepr state
(finState, (result, notify)) = updateLocksWaiting prio owner req state
(finState', (result', notify')) = updateLocksWaiting prio owner req state'
in counterexample "extRepr-equal states must behave equal on updateLocks"
$ and [ result == result'
, notify == notify'
, extRepr finState == extRepr finState'
]
prop_SafeUpdateWaitingCorrect :: Property
prop_SafeUpdateWaitingCorrect =
forAll (arbitrary :: Gen TestOwner) $ \owner ->
forAll ((arbitrary :: Gen (LockWaiting TestLock TestOwner Integer))
`suchThat` (not . hasPendingRequest owner)) $ \state ->
forAll (arbitrary :: Gen Integer) $ \prio ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
let (state', answer') = updateLocksWaiting prio owner req state
(state'', answer'') = safeUpdateLocksWaiting prio owner req state
in conjoin [ counterexample ("safeUpdateLocksWaiting gave different answer: "
++ show answer' ++ " /= " ++ show answer'')
$ answer' == answer''
, counterexample ("safeUpdateLocksWaiting gave different states\
\ after answer " ++ show answer' ++ ": "
++ show (extRepr state') ++ " /= "
++ show (extRepr state''))
$ extRepr state' == extRepr state''
]
prop_SafeUpdateWaitingIdempotent :: Property
prop_SafeUpdateWaitingIdempotent =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \owner ->
forAll (arbitrary :: Gen Integer) $ \prio ->
forAll (arbitrary :: Gen [LockRequest TestLock]) $ \req ->
let (state', (answer', _)) = safeUpdateLocksWaiting prio owner req state
(state'', (answer'', nfy)) = safeUpdateLocksWaiting prio owner req state'
in conjoin [ counterexample ("repeated safeUpdateLocks waiting gave different\
\ answers: " ++ show answer' ++ " /= "
++ show answer'') $ answer' == answer''
, counterexample "safeUpdateLocksWaiting not idempotent"
$ extRepr state' == extRepr state''
, counterexample ("notifications (" ++ show nfy ++ ") on replay")
$ S.null nfy
]
prop_ReadShow :: Property
prop_ReadShow =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
(liftM extRepr . J.readJSON $ J.showJSON state) ==? (J.Ok $ extRepr state)
prop_OpportunisticMonotone :: Property
prop_OpportunisticMonotone =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \a ->
forAll ((choose (1,3) >>= vector) :: Gen [(TestLock, L.OwnerState)]) $ \req ->
let (state', _) = opportunisticLockUnion a req state
oldOwned = listLocks a $ getAllocation state
oldLocks = M.keys oldOwned
newOwned = listLocks a $ getAllocation state'
in counterexample "Opportunistic union may only increase the set of locks\
\ held"
. flip all oldLocks $ \lock ->
M.lookup lock newOwned >= M.lookup lock oldOwned
prop_OpportunisticAnswer :: Property
prop_OpportunisticAnswer =
forAll (arbitrary :: Gen (LockWaiting TestLock TestOwner Integer)) $ \state ->
forAll (arbitrary :: Gen TestOwner) $ \a ->
forAll ((choose (1,3) >>= vector) :: Gen [(TestLock, L.OwnerState)]) $ \req ->
let (state', (result, _)) = opportunisticLockUnion a req state
oldOwned = listLocks a $ getAllocation state
newOwned = listLocks a $ getAllocation state'
involvedLocks = M.keys oldOwned ++ map fst req
in conjoin [ counterexample ("Locks not in the answer set " ++ show result
++ " may not be changed, but found "
++ show state')
. flip all involvedLocks $ \lock ->
(lock `elem` result)
|| (M.lookup lock oldOwned == M.lookup lock newOwned)
, counterexample ("Locks not in the answer set " ++ show result
++ " must be as requested, but found "
++ show state')
. flip all involvedLocks $ \lock ->
notElem lock result
|| maybe False (flip elem req . (,) lock)
(M.lookup lock newOwned)
]
testSuite "Locking/Waiting"
[ 'prop_NoActionWithPendingRequests
, 'prop_WaitingRequestsGetPending
, 'prop_PendingGetFulfilledEventually
, 'prop_PendingGetNotifiedEventually
, 'prop_Progress
, 'prop_ProgressSound
, 'prop_PendingJustified
, 'prop_extReprPreserved
, 'prop_UpdateIdempotent
, 'prop_SimulateUpdateLocks
, 'prop_SimulateUpdateLocksWaiting
, 'prop_ReadShow
, 'prop_SafeUpdateWaitingCorrect
, 'prop_SafeUpdateWaitingIdempotent
, 'prop_OpportunisticMonotone
, 'prop_OpportunisticAnswer
]