{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-| Tests for lock allocation. -} {- Copyright (C) 2014 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -} module Test.Ganeti.Locking.Allocation ( testLocking_Allocation , TestLock , TestOwner , requestSucceeded ) where import Prelude () import Ganeti.Prelude import qualified Data.Foldable as F import qualified Data.Map as M import Data.Maybe (fromMaybe) import qualified Data.Set as S import qualified Text.JSON as J import Test.QuickCheck import Test.Ganeti.TestCommon import Test.Ganeti.TestHelper import Ganeti.BasicTypes import Ganeti.Locking.Allocation import Ganeti.Locking.Types {- Ganeti.Locking.Allocation is polymorphic in the types of locks and lock owners. So we can use much simpler types here than Ganeti's real locks and lock owners, knowning that polymorphic functions cannot exploit the simplicity of the types they're deling with. -} data TestOwner = TestOwner Int deriving (Ord, Eq, Show) instance Arbitrary TestOwner where arbitrary = TestOwner <$> choose (0, 2) data TestLock = TestBigLock | TestCollectionLockA | TestLockA Int | TestCollectionLockB | TestLockB Int deriving (Ord, Eq, Show, Read) instance Arbitrary TestLock where arbitrary = frequency [ (1, elements [ TestBigLock , TestCollectionLockA , TestCollectionLockB ]) , (2, TestLockA <$> choose (0, 2)) , (2, TestLockB <$> choose (0, 2)) ] instance Lock TestLock where lockImplications (TestLockA _) = [TestCollectionLockA, TestBigLock] lockImplications (TestLockB _) = [TestCollectionLockB, TestBigLock] lockImplications TestBigLock = [] lockImplications _ = [TestBigLock] {- All states of a LockAllocation ever available outside the Ganeti.Locking.Allocation module must be constructed by starting with emptyAllocation and applying the exported functions. -} instance Arbitrary OwnerState where arbitrary = elements [OwnShared, OwnExclusive] instance Arbitrary a => Arbitrary (LockRequest a) where arbitrary = LockRequest <$> arbitrary <*> genMaybe arbitrary data UpdateRequest b a = UpdateRequest b [LockRequest a] | FreeLockRequest b deriving Show instance (Arbitrary a, Arbitrary b) => Arbitrary (UpdateRequest a b) where arbitrary = frequency [ (4, UpdateRequest <$> arbitrary <*> (choose (1, 4) >>= vector)) , (1, FreeLockRequest <$> arbitrary) ] -- | Transform an UpdateRequest into the corresponding state transformer. asAllocTrans :: (Lock a, Ord b, Show b) => LockAllocation a b -> UpdateRequest b a -> LockAllocation a b asAllocTrans state (UpdateRequest owner updates) = fst $ updateLocks owner updates state asAllocTrans state (FreeLockRequest owner) = freeLocks state owner -- | Fold a sequence of requests to transform a lock allocation onto the empty -- allocation. As we consider all exported LockAllocation transformers, any -- LockAllocation definable is obtained in this way. foldUpdates :: (Lock a, Ord b, Show b) => [UpdateRequest b a] -> LockAllocation a b foldUpdates = foldl asAllocTrans emptyAllocation instance (Arbitrary a, Lock a, Arbitrary b, Ord b, Show b) => Arbitrary (LockAllocation a b) where arbitrary = foldUpdates <$> (choose (0, 8) >>= vector) -- | Basic property of locking: the exclusive locks of one user -- are disjoint from any locks of any other user. prop_LocksDisjoint :: Property prop_LocksDisjoint = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary `suchThat` (/= a)) $ \b -> let aExclusive = M.keysSet . M.filter (== OwnExclusive) $ listLocks a state bAll = M.keysSet $ listLocks b state in counterexample (show a ++ "'s exclusive lock" ++ " is not respected by " ++ show b) (S.null $ S.intersection aExclusive bAll) -- | Verify that the list of active locks indeed contains all locks that -- are owned by someone. prop_LockslistComplete :: Property prop_LockslistComplete = forAll (arbitrary :: Gen TestOwner) $ \a -> forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (not . M.null . listLocks a)) $ \state -> counterexample "All owned locks must be mentioned in the all-locks list" $ let allLocks = listAllLocks state in all (`elem` allLocks) (M.keys $ listLocks a state) -- | Verify that the list of all locks with states is contained in the list -- of all locks. prop_LocksAllOwnersSubsetLockslist :: Property prop_LocksAllOwnersSubsetLockslist = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> counterexample "The list of all active locks must contain all locks mentioned\ \ in the locks state" $ S.isSubsetOf (S.fromList . map fst $ listAllLocksOwners state) (S.fromList $ listAllLocks state) -- | Verify that all locks of all owners are mentioned in the list of all locks' -- owner's state. prop_LocksAllOwnersComplete :: Property prop_LocksAllOwnersComplete = forAll (arbitrary :: Gen TestOwner) $ \a -> forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (not . M.null . listLocks a)) $ \state -> counterexample "Owned locks must be mentioned in list of all locks' state" $ let allLocksState = listAllLocksOwners state in flip all (M.toList $ listLocks a state) $ \(lock, ownership) -> elem (a, ownership) . fromMaybe [] $ lookup lock allLocksState -- | Verify that all lock owners mentioned in the list of all locks' owner's -- state actually own their lock. prop_LocksAllOwnersSound :: Property prop_LocksAllOwnersSound = forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (not . null . listAllLocksOwners)) $ \state -> counterexample "All locks mentioned in listAllLocksOwners must be owned by\ \ the mentioned owner" . flip all (listAllLocksOwners state) $ \(lock, owners) -> flip all owners $ \(owner, ownership) -> holdsLock owner lock ownership state -- | Verify that exclusive group locks are honored, i.e., verify that if someone -- holds a lock, then no one else can hold a lock on an exclusive lock on an -- implied lock. prop_LockImplicationX :: Property prop_LockImplicationX = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary `suchThat` (/= a)) $ \b -> let bExclusive = M.keysSet . M.filter (== OwnExclusive) $ listLocks b state in counterexample "Others cannot have an exclusive lock on an implied lock" . flip all (M.keys $ listLocks a state) $ \lock -> flip all (lockImplications lock) $ \impliedlock -> not $ S.member impliedlock bExclusive -- | Verify that shared group locks are honored, i.e., verify that if someone -- holds an exclusive lock, then no one else can hold any form on lock on an -- implied lock. prop_LockImplicationS :: Property prop_LockImplicationS = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary `suchThat` (/= a)) $ \b -> let aExclusive = M.keys . M.filter (== OwnExclusive) $ listLocks a state bAll = M.keysSet $ listLocks b state in counterexample "Others cannot hold locks implied by an exclusive lock" . flip all aExclusive $ \lock -> flip all (lockImplications lock) $ \impliedlock -> not $ S.member impliedlock bAll -- | Verify that locks can only be modified by updates of the owner. prop_LocksStable :: Property prop_LocksStable = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary `suchThat` (/= a)) $ \b -> forAll (arbitrary :: Gen [LockRequest TestLock]) $ \request -> let (state', _) = updateLocks b request state in (listLocks a state ==? listLocks a state') -- | Verify that a given request is statisfied in list of owned locks requestSucceeded :: Ord a => M.Map a OwnerState -> LockRequest a -> Bool requestSucceeded owned (LockRequest lock status) = M.lookup lock owned == status -- | Verify that lock updates are atomic, i.e., either we get all the required -- locks, or the state is completely unchanged. prop_LockupdateAtomic :: Property prop_LockupdateAtomic = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary :: Gen [LockRequest TestLock]) $ \request -> let (state', result) = updateLocks a request state in if result == Ok S.empty then counterexample ("Update succeeded, but in final state " ++ show state' ++ "not all locks are as requested") $ let owned = listLocks a state' in all (requestSucceeded owned) request else counterexample ("Update failed, but state changed to " ++ show state') (state == state') -- | Verify that releasing a lock always succeeds. prop_LockReleaseSucceeds :: Property prop_LockReleaseSucceeds = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary :: Gen TestLock) $ \lock -> let (_, result) = updateLocks a [requestRelease lock] state in counterexample ("Releasing a lock has to suceed uncondiationally, but got " ++ show result) (isOk result) -- | Verify the property that only the blocking owners prevent -- lock allocation. We deliberatly go for the expensive variant -- restraining by suchThat, as otherwise the number of cases actually -- covered is too small. prop_BlockSufficient :: Property prop_BlockSufficient = forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary :: Gen TestLock) $ \lock -> forAll (elements [ [requestShared lock] , [requestExclusive lock]]) $ \request -> forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (genericResult (const False) (not . S.null) . snd . updateLocks a request)) $ \state -> let (_, result) = updateLocks a request state blockedOn = genericResult (const S.empty) id result in counterexample "After all blockers release, a request must succeed" . isOk . snd . updateLocks a request $ F.foldl freeLocks state blockedOn -- | Verify the property that every blocking owner is necessary, i.e., even -- if we only keep the locks of one of the blocking owners, the request still -- will be blocked. We deliberatly use the expensive variant of restraining -- to ensure good coverage. To make sure the request can always be blocked -- by two owners, for a shared request we request two different locks. prop_BlockNecessary :: Property prop_BlockNecessary = forAll (arbitrary :: Gen TestOwner) $ \a -> forAll (arbitrary :: Gen TestLock) $ \lock -> forAll (arbitrary `suchThat` (/= lock)) $ \lock' -> forAll (elements [ [requestShared lock, requestShared lock'] , [requestExclusive lock]]) $ \request -> forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (genericResult (const False) ((>= 2) . S.size) . snd . updateLocks a request)) $ \state -> let (_, result) = updateLocks a request state blockers = genericResult (const S.empty) id result in counterexample "Each blocker alone must block the request" . flip all (S.elems blockers) $ \blocker -> (==) (Ok $ S.singleton blocker) . snd . updateLocks a request . F.foldl freeLocks state $ S.filter (/= blocker) blockers instance J.JSON TestOwner where showJSON (TestOwner x) = J.showJSON x readJSON = (>>= return . TestOwner) . J.readJSON instance J.JSON TestLock where showJSON = J.showJSON . show readJSON = (>>= return . read) . J.readJSON -- | Verify that for LockAllocation we have readJSON . showJSON = Ok. prop_ReadShow :: Property prop_ReadShow = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> J.readJSON (J.showJSON state) ==? J.Ok state -- | Verify that the list of lock owners is complete. prop_OwnerComplete :: Property prop_OwnerComplete = forAll (arbitrary :: Gen (LockAllocation TestLock TestOwner)) $ \state -> foldl freeLocks state (lockOwners state) ==? emptyAllocation -- | Verify that each owner actually owns a lock. prop_OwnerSound :: Property prop_OwnerSound = forAll ((arbitrary :: Gen (LockAllocation TestLock TestOwner)) `suchThat` (not . null . lockOwners)) $ \state -> counterexample "All subjects listed as owners must own at least one lock" . flip all (lockOwners state) $ \owner -> not . M.null $ listLocks owner state -- | Verify that for LockRequest we have readJSON . showJSON = Ok. prop_ReadShowRequest :: Property prop_ReadShowRequest = forAll (arbitrary :: Gen (LockRequest TestLock)) $ \state -> J.readJSON (J.showJSON state) ==? J.Ok state testSuite "Locking/Allocation" [ 'prop_LocksDisjoint , 'prop_LockslistComplete , 'prop_LocksAllOwnersSubsetLockslist , 'prop_LocksAllOwnersComplete , 'prop_LocksAllOwnersSound , 'prop_LockImplicationX , 'prop_LockImplicationS , 'prop_LocksStable , 'prop_LockupdateAtomic , 'prop_LockReleaseSucceeds , 'prop_BlockSufficient , 'prop_BlockNecessary , 'prop_ReadShow , 'prop_OwnerComplete , 'prop_OwnerSound , 'prop_ReadShowRequest ]