linear-base-0.8.1: test/Test/Data/Mutable/HashMap.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
-- |
-- Tests for mutable hashmaps
--
-- See the testing framework explained in Test.Data.Mutable.Set.
--
-- The combination of axioms and homomorphisms provided (for the most part)
-- functionally specify the behavior of hashmaps. There are a few things
-- we leave out and mention below.
--
-- Remarks:
-- * We don't test trivial things like: empty, capacity
-- * We don't test member since we test lookup
-- * We don't test alter and hope insert and delete tests suffice
-- * We don't test filterWithKey and hope the test for filter suffices
-- * We don't test mapMaybe since mapMaybeWithKey is more general
module Test.Data.Mutable.HashMap
( mutHMTests,
)
where
import Data.Containers.ListUtils (nubOrdOn)
import Data.Function ((&))
import qualified Data.Functor.Linear as Linear
import qualified Data.HashMap.Mutable.Linear as HashMap
import Data.List (sort)
import qualified Data.List as List
import qualified Data.Map.Lazy as Map
import Data.Maybe (mapMaybe)
import Data.Unrestricted.Linear
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified Prelude.Linear as Linear
import Test.Tasty
import Test.Tasty.Hedgehog (testPropertyNamed)
-- # Exported Tests
--------------------------------------------------------------------------------
mutHMTests :: TestTree
mutHMTests = testGroup "Mutable hashmap tests" group
group :: [TestTree]
group =
[ -- Axiomatic tests
testPropertyNamed "∀ k,v,m. lookup k (insert m k v) = Just v" "lookupInsert1" lookupInsert1,
testPropertyNamed
"∀ k,v,m,k'/=k. lookup k'(insert m k v) = lookup k' m"
"lookuInsert2"
lookupInsert2,
testPropertyNamed "∀ k,m. lookup k (delete m k) = Nothing" "lookupDelete1" lookupDelete1,
testPropertyNamed
"∀ k,m,k'/=k. lookup k' (delete m k) = lookup k' m"
"lookupDelete2"
lookupDelete2,
testPropertyNamed "∀ k,v,m. member k (insert m k v) = True" "memberInsert" memberInsert,
testPropertyNamed "∀ k,m. member k (delete m k) = False" "memberDelete" memberDelete,
testPropertyNamed "∀ k,v,m. size (insert (m-k) k v) = 1+ size (m-k)" "sizeInsert" sizeInsert,
testPropertyNamed "∀ k,m with k. size (delete m k) + 1 = size m" "deleteSize" deleteSize,
-- Homorphism tests against a reference implementation
testPropertyNamed "insert k v h = fromList (toList h ++ [(k,v)])" "refInsert" refInsert,
testPropertyNamed "delete k h = fromList (filter (!= k . fst) (toList h))" "refDelete" refDelete,
testPropertyNamed "fst . lookup k h = lookup k (toList h)" "refLookup" refLookup,
testPropertyNamed "mapMaybe f h = fromList . mapMaybe (uncurry f) . toList" "refMap" refMap,
testPropertyNamed "size = length . toList" "refSize" refSize,
testPropertyNamed "toList . fromList = id" "refToListFromList" refToListFromList,
testPropertyNamed "filter f (fromList xs) = fromList (filter f xs)" "refFilter" refFilter,
testPropertyNamed "fromList xs <> fromList ys = fromList (xs <> ys)" "refMappend" refMappend,
testPropertyNamed "unionWith reference" "refUnionWith" refUnionWith,
testPropertyNamed "intersectionWith reference" "refIntersectionWith" refIntersectionWith,
-- Misc
testPropertyNamed "toList . shrinkToFit = toList" "shrinkToFitTest" shrinkToFitTest
]
-- # Internal Library
--------------------------------------------------------------------------------
-- # Mini Testing Framework
----------------------------------------
-- | All tests are on maps from int to string
type HMap = HashMap.HashMap Int String
-- | A test checks a boolean property on a hashmap and consumes it
type HMTest = HMap %1 -> Ur Bool
maxSize :: Int
maxSize = 800
-- HashMap's have lots of corner cases, so we try harder to find them.
defProperty :: PropertyT IO () -> Property
defProperty = withTests 1000 . property
-- | Run a test on a random HashMap
testOnAnyHM :: PropertyT IO HMTest -> Property
testOnAnyHM propHmtest = defProperty $ do
kvs <- forAll keyVals
hmtest <- propHmtest
assert $ unur Linear.$ HashMap.fromList kvs hmtest
testKVPairExists :: (Int, String) -> HMTest
testKVPairExists (k, v) hmap =
fromLookup Linear.$ getFst Linear.$ HashMap.lookup k hmap
where
fromLookup :: Ur (Maybe String) %1 -> Ur Bool
fromLookup (Ur Nothing) = Ur False
fromLookup (Ur (Just v')) = Ur (v' == v)
testKeyMember :: Int -> HMTest
testKeyMember key hmap = getFst Linear.$ HashMap.member key hmap
testKeyNotMember :: Int -> HMTest
testKeyNotMember key hmap = Linear.fmap Linear.not (testKeyMember key hmap)
-- | That is, test that lookup gives us `Nothing`
testKeyMissing :: Int -> HMTest
testKeyMissing key hmap =
fromLookup Linear.$ getFst Linear.$ HashMap.lookup key hmap
where
fromLookup :: Ur (Maybe String) %1 -> Ur Bool
fromLookup (Ur Nothing) = Ur True
fromLookup (Ur _) = Ur False
testLookupUnchanged :: (HMap %1 -> HMap) -> Int -> HMTest
testLookupUnchanged f k hmap = fromLookup (HashMap.lookup k hmap)
where
fromLookup :: (Ur (Maybe String), HMap) %1 -> Ur Bool
fromLookup (look1, hmap') =
compareMaybes look1 (getFst Linear.$ HashMap.lookup k (f hmap'))
insertPair :: (Int, String) -> HMap %1 -> HMap
insertPair (k, v) hmap = HashMap.insert k v hmap
-- XXX: This is a terrible name
getFst :: (Consumable b) => (a, b) %1 -> a
getFst (a, b) = lseq b a
compareMaybes ::
(Eq a) =>
Ur (Maybe a) %1 ->
Ur (Maybe a) %1 ->
Ur Bool
compareMaybes (Ur a) (Ur b) = Ur (a == b)
-- # Random Generation
----------------------------------------
-- | Key generator
key :: Gen Int
key = Gen.int $ Range.linearFrom 0 (-20) 20
-- | Value generator
val :: Gen String
val = do
let strSize = Range.singleton 3
Gen.string strSize Gen.alpha
-- | Random pairs with no duplicate keys
keyVals :: Gen [(Int, String)]
keyVals = do
size <- Gen.int $ Range.linear 0 maxSize
let sizeGen = Range.singleton size
keys <- Gen.list sizeGen key
vals <- Gen.list sizeGen val
return $ zip keys vals
-- # Tests
--------------------------------------------------------------------------------
lookupInsert1 :: Property
lookupInsert1 = testOnAnyHM $ do
k <- forAll key
v <- forAll val
let insertKV = insertPair (k, v)
let checkKV = testKVPairExists (k, v)
return (checkKV Linear.. insertKV)
lookupInsert2 :: Property
lookupInsert2 = testOnAnyHM $ do
k <- forAll key
k' <- forAll $ Gen.filter (/= k) key
v <- forAll val
let insertKV = insertPair (k, v)
return (testLookupUnchanged insertKV k')
lookupDelete1 :: Property
lookupDelete1 = testOnAnyHM $ do
k <- forAll key
let checkNoKey = testKeyMissing k
return (checkNoKey Linear.. HashMap.delete k)
lookupDelete2 :: Property
lookupDelete2 = testOnAnyHM $ do
k <- forAll key
k' <- forAll $ Gen.filter (/= k) key
return (testLookupUnchanged (HashMap.delete k) k')
memberInsert :: Property
memberInsert = testOnAnyHM $ do
k <- forAll key
v <- forAll val
let insertKV = insertPair (k, v)
let isMemberK = testKeyMember k
return (isMemberK Linear.. insertKV)
memberDelete :: Property
memberDelete = testOnAnyHM $ do
k <- forAll key
v <- forAll val
let pair = (k, v)
let insertKV = insertPair pair
let checkNotMember = testKeyNotMember k
return (checkNotMember Linear.. HashMap.delete k Linear.. insertKV)
sizeInsert :: Property
sizeInsert = testOnAnyHM $ do
k <- forAll key
v <- forAll val
let pair = (k, v)
let insertCheckSize = checkSizeAfterInsert pair
return (insertCheckSize Linear.. HashMap.delete k)
checkSizeAfterInsert :: (Int, String) -> HMTest
checkSizeAfterInsert (k, v) hmap = withSize Linear.$ HashMap.size hmap
where
withSize :: (Ur Int, HMap) %1 -> Ur Bool
withSize (oldSize, hmap) =
checkSize oldSize Linear.$
getFst Linear.$
HashMap.size Linear.$
HashMap.insert k v hmap
checkSize :: Ur Int %1 -> Ur Int %1 -> Ur Bool
checkSize (Ur old) (Ur new) =
Ur ((old + 1) == new)
deleteSize :: Property
deleteSize = testOnAnyHM $ do
k <- forAll key
v <- forAll val
let insertKV = insertPair (k, v)
let checkSize = checkSizeAfterDelete k
return (checkSize Linear.. insertKV)
checkSizeAfterDelete :: Int -> HMTest
checkSizeAfterDelete key hmap = fromSize (HashMap.size hmap)
where
fromSize :: (Ur Int, HMap) %1 -> Ur Bool
fromSize (orgSize, hmap) =
compSizes orgSize Linear.$
getFst Linear.$
HashMap.size (HashMap.delete key hmap)
compSizes :: Ur Int %1 -> Ur Int %1 -> Ur Bool
compSizes (Ur orgSize) (Ur newSize) =
Ur ((newSize + 1) == orgSize)
refInsert :: Property
refInsert = defProperty $ do
k <- forAll key
v <- forAll val
kvs <- forAll keyVals
let listInsert = HashMap.fromList (kvs ++ [(k, v)]) HashMap.toList
let hmInsert = HashMap.fromList kvs (HashMap.toList Linear.. HashMap.insert k v)
sort (unur listInsert) === sort (unur hmInsert)
refDelete :: Property
refDelete = defProperty $ do
k <- forAll key
kvs <- forAll keyVals
let kvs' = filter ((/= k) . fst) kvs
let listInsert = HashMap.fromList kvs' HashMap.toList
let hmInsert = HashMap.fromList kvs (HashMap.toList Linear.. HashMap.delete k)
sort (unur listInsert) === sort (unur hmInsert)
refLookup :: Property
refLookup = defProperty $ do
kvs <- forAll keyVals
k <- forAll key
let listLookup = List.lookup k (List.reverse kvs)
let hmLookup = HashMap.fromList kvs (getFst Linear.. HashMap.lookup k)
listLookup === unur hmLookup
refMap :: Property
refMap = defProperty $ do
let f k v = if mod k 5 < 3 then Just (show k ++ v) else Nothing
let f' (k, v) = fmap ((,) k) (f k v)
kvs <- forAll keyVals
let mappedList = mapMaybe f' (nubOrdOn fst (List.reverse kvs))
let mappedHm = HashMap.fromList kvs (HashMap.toList Linear.. HashMap.mapMaybeWithKey f)
sort mappedList === sort (unur mappedHm)
refSize :: Property
refSize = defProperty $ do
kvs <- forAll keyVals
length (nubOrdOn fst kvs) === unur (HashMap.fromList kvs (getFst Linear.. HashMap.size))
refToListFromList :: Property
refToListFromList = defProperty $ do
xs <- forAll keyVals
let expected =
Map.fromList xs
& Map.toList
Ur actual = HashMap.fromList xs HashMap.toList
sort expected === sort actual
refFilter :: Property
refFilter = defProperty $ do
xs <- forAll keyVals
let predicate "" = False
predicate (i : _) = i < 'h'
expected =
Map.fromList xs
& Map.filter predicate
& Map.toList
Ur actual =
HashMap.fromList xs Linear.$
HashMap.toList Linear.. HashMap.filter predicate
sort expected === sort actual
refMappend :: Property
refMappend = defProperty $ do
xs <- forAll keyVals
ys <- forAll keyVals
let Ur expected =
HashMap.fromList (xs <> ys) Linear.$ HashMap.toList
Ur actual =
HashMap.fromList xs Linear.$ \hx ->
HashMap.fromList ys Linear.$ \hy ->
HashMap.toList (hx Linear.<> hy)
sort expected === sort actual
refUnionWith :: Property
refUnionWith = defProperty $ do
xs <- forAll keyVals
ys <- forAll keyVals
let combine a b = a ++ "," ++ b
expected =
Map.unionWith
combine
(Map.fromList xs)
(Map.fromList ys)
& Map.toList
Ur actual =
HashMap.fromList xs Linear.$ \hx ->
HashMap.fromList ys Linear.$ \hy ->
HashMap.unionWith combine hx hy
Linear.& HashMap.toList
sort expected === sort actual
refIntersectionWith :: Property
refIntersectionWith = defProperty $ do
xs <- forAll keyVals
ys <- forAll keyVals
let expected =
Map.intersectionWith
(,)
(Map.fromList xs)
(Map.fromList ys)
& Map.toList
Ur actual =
HashMap.fromList xs Linear.$ \hx ->
HashMap.fromList ys Linear.$ \hy ->
HashMap.intersectionWith (,) hx hy
Linear.& HashMap.toList
sort expected === sort actual
shrinkToFitTest :: Property
shrinkToFitTest = defProperty $ do
kvs <- forAll keyVals
let shrunk = (HashMap.fromList kvs (HashMap.toList Linear.. HashMap.shrinkToFit))
sort (nubOrdOn fst (List.reverse kvs)) === sort (unur shrunk)