|
| 1 | +{-# LANGUAGE CPP, GeneralizedNewtypeDeriving #-} |
| 2 | + |
| 3 | +-- | Tests for size field invariant in 'HashMap' wrapper introduced in GitHub |
| 4 | +-- PR #170. |
| 5 | + |
| 6 | +module Main (main) where |
| 7 | + |
| 8 | +import Control.Monad.ST (ST) |
| 9 | +#if defined(STRICT) |
| 10 | +import qualified Data.HashMap.Strict as HM |
| 11 | +#else |
| 12 | +import qualified Data.HashMap.Lazy as HM |
| 13 | +#endif |
| 14 | +import Data.List (scanl') |
| 15 | + |
| 16 | +import Test.QuickCheck (Arbitrary, Property, conjoin, oneof) |
| 17 | +import Test.Framework (Test, defaultMain, testGroup) |
| 18 | + |
| 19 | +-- Key type that generates more hash collisions. |
| 20 | +newtype Key = K { unK :: Int } |
| 21 | + deriving (Arbitrary, Eq, Ord, Read, Show) |
| 22 | + |
| 23 | +instance Hashable Key where |
| 24 | + hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20 |
| 25 | + |
| 26 | +-- Datatype representing the actions that can potentially change a hashmap's |
| 27 | +-- size modulo repetition i.e. 'mapMaybe' and 'filter' are essentially |
| 28 | +-- equivalent in how they modify a hashmap, so only one ('filter') is tested. |
| 29 | +data HashMapAction = |
| 30 | + | Insert Key Int |
| 31 | + | Delete Key |
| 32 | + | Union (HM.HashMap Key Int) |
| 33 | + | Intersection (HM.HashMap Key Int) |
| 34 | + | Difference (HM.HashMap Key Int) |
| 35 | + | Filter (Int -> Bool) |
| 36 | + |
| 37 | +instance Arbitrary HashMapAction where |
| 38 | + arbitrary = oneof |
| 39 | + [ Insert <$> arbitrary <*> arbitrary |
| 40 | + , Delete <$> arbitrary |
| 41 | + , Union <$> arbitrary |
| 42 | + , Intersection <$> arbitrary |
| 43 | + , Difference <$> arbitrary |
| 44 | + , pure $ Filter even |
| 45 | + ] |
| 46 | + |
| 47 | +-- Simple way of representing a hashmap and its size without having to |
| 48 | +-- use 'size', which is the function to be tested. As such, its use is |
| 49 | +-- avoided and the 'Int' field of the tuple is used instead. |
| 50 | +type HashMapSt = (Int, HM.HashMap Key Int) |
| 51 | + |
| 52 | +-- | Applies a 'HashMapAction' to 'HashMapSt', updating the hashmap's |
| 53 | +-- size after the operation. |
| 54 | +applyActionToState :: HashMapSt -> HashMapAction -> HashMapSt |
| 55 | +applyActionToState p@(sz, hm) (Insert k v) |
| 56 | + | HM.member k hm = (sz + 1, HM.insert k v hm) |
| 57 | + | otherwise = p |
| 58 | +applyActionToState p@(sz, hm) (Delete k) |
| 59 | + | HM.member k hm = p |
| 60 | + Nothing -> p |
| 61 | + | otherwise = (sz - 1, HM.delete k hm) |
| 62 | +applyActionToState (sz, hm) (Union hm') = |
| 63 | + let sz' = length $ HM.toList hm' |
| 64 | + lenIntersect = length [ k | k <- HM.keys hm, HM.member k hm' ] |
| 65 | + newLen = sz + sz' - lenIntersect |
| 66 | + in (newLen, HM.union hm hm') |
| 67 | +applyActionToState (sz, hm) (Intersection hm') = |
| 68 | + let lenIntersect = length [ k | k <- HM.keys hm, HM.member k hm' ] |
| 69 | + in (lenIntersect, HM.intersect hm hm') |
| 70 | +applyActionToState (sz, hm) (Difference hm')= |
| 71 | + let lenDiff = length [ k | k <- HM.keys hm, not $ HM.member k hm' ] |
| 72 | + (lenDiff, HM.difference hm hm') |
| 73 | +applyActionToState (sz, hm) (Filter pred) = |
| 74 | + let lenFilter = length [ (k, v) | (k, v) <- HM.elems hm, pred v ] |
| 75 | + in (lenFilter, HM.filter pred hm) |
| 76 | + |
| 77 | +-- | Property to check that after each operation that may change a hashmap's |
| 78 | +-- size, the 'Int' field in the 'HashMap' wrapper always correctly represents |
| 79 | +-- the hashmap's size. |
| 80 | +sizeInvariantProperty :: [HashMapAction] -> Property |
| 81 | +sizeInvariantProperty actionList = |
| 82 | + conjoin . |
| 83 | + map (\(sz, hm) -> sz == HM.size hm) . |
| 84 | + scanl' applyActionToState (0, mempty) $ actionList |
| 85 | + |
| 86 | +------------------------------------------------------------------------ |
| 87 | +-- * Test list |
| 88 | + |
| 89 | +tests :: [Test] |
| 90 | +tests = [ |
| 91 | + testGroup "size invariant checks" |
| 92 | + [ sizeInvariantProperty |
| 93 | + ] |
| 94 | + ] |
| 95 | + |
| 96 | +------------------------------------------------------------------------ |
| 97 | +-- * Test harness |
| 98 | + |
| 99 | +main :: IO () |
| 100 | +main = defaultMain tests |
0 commit comments