vector-hashtables-0.1.0.0: test/Data/Vector/HashTablesSpec.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Data.Vector.HashTablesSpec where
import Control.Monad.Primitive
import Data.Hashable (Hashable (hashWithSalt))
import qualified Data.List as L
import Data.Primitive.MutVar
import Data.Proxy (Proxy (..))
import qualified Data.Set as Set
import Data.Vector.Generic (Mutable, Vector)
import qualified Data.Vector.Generic as VI
import Data.Vector.Generic.Mutable (MVector)
import qualified Data.Vector.Mutable as M
import qualified Data.Vector.Storable.Mutable as SM
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as UM
import GHC.Generics (Generic)
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck (Arbitrary (..), Gen,
NonNegative (..),
Positive (..), Property,
choose, elements, forAll,
generate, property, shuffle,
vector)
import Test.Hspec (Spec, describe, errorCall, it,
shouldBe, shouldThrow)
import qualified Data.Vector.Hashtables.Internal as VH
newtype AlwaysCollide = AC Int
deriving newtype (Arbitrary, SM.Storable, Num, Eq, Ord, Show)
deriving stock Generic
instance Hashable AlwaysCollide where
hashWithSalt _ _ = 1
listN :: Int -> Gen [(Int, Int)]
listN n = do
keys <- vector n
vals <- vector n
let keys' = Set.toList (Set.fromList keys)
return (zip keys' vals)
shuffledListN :: Int -> Gen ([(Int, Int)], [(Int, Int)])
shuffledListN n = do
testData <- listN n
shuffledTestData <- shuffle testData
return (testData, shuffledTestData)
listsForRemoveN :: Int -> Gen ([(Int, Int)], [Int])
listsForRemoveN n = do
testData <- listN n
dropCount <- min (n - 1) <$> choose (1, n)
let deleteData = fst <$> take dropCount testData
return (testData, deleteData)
twoListsN :: Int -> Gen ([(Int, Int)], [(Int, Int)])
twoListsN n = do
list1 <- listN n
list2 <- listN n
return (list1, list2)
spec :: Spec
spec = mutableSpec
*> storableMutableSpec
*> storableKeysSpec
*> unboxedKeysSpec
class HashTableTest ks vs where
specDescription :: Proxy ks -> Proxy vs -> String
testInit :: Proxy ks -> Proxy vs -> Int -> IO (VH.Dictionary (PrimState IO) ks Int vs Int)
testInsert :: (VH.Dictionary (PrimState IO) ks Int vs Int) -> Int -> Int -> IO ()
testAt :: (VH.Dictionary (PrimState IO) ks Int vs Int) -> Int -> IO Int
testAt' :: (VH.Dictionary (PrimState IO) ks Int vs Int) -> Int -> IO (Maybe Int)
testDelete :: (VH.Dictionary (PrimState IO) ks Int vs Int) -> Int -> IO ()
testInitCollide
:: Proxy ks
-> Proxy vs
-> Int
-> IO (VH.Dictionary (PrimState IO) ks AlwaysCollide vs Int)
testInsertCollide
:: (VH.Dictionary (PrimState IO) ks AlwaysCollide vs Int) -> AlwaysCollide -> Int -> IO ()
testAtCollide
:: (VH.Dictionary (PrimState IO) ks AlwaysCollide vs Int) -> AlwaysCollide -> IO Int
testFromList
:: Proxy ks -> Proxy vs -> [(Int, Int)] -> IO (VH.Dictionary (PrimState IO) ks Int vs Int)
testToList :: VH.Dictionary (PrimState IO) ks Int vs Int -> IO [(Int, Int)]
testLength :: VH.Dictionary (PrimState IO) ks Int vs Int -> IO Int
testNull :: VH.Dictionary (PrimState IO) ks Int vs Int -> IO Bool
testMember :: VH.Dictionary (PrimState IO) ks Int vs Int -> Int -> IO Bool
testAlter :: VH.Dictionary (PrimState IO) ks Int vs Int -> (Maybe Int -> Maybe Int) -> Int -> IO ()
testUnion
:: VH.Dictionary (PrimState IO) ks Int vs Int
-> VH.Dictionary (PrimState IO) ks Int vs Int
-> IO (VH.Dictionary (PrimState IO) ks Int vs Int)
testDifference
:: VH.Dictionary (PrimState IO) ks Int vs Int
-> VH.Dictionary (PrimState IO) ks Int vs Int
-> IO (VH.Dictionary (PrimState IO) ks Int vs Int)
testIntersection
:: VH.Dictionary (PrimState IO) ks Int vs Int
-> VH.Dictionary (PrimState IO) ks Int vs Int
-> IO (VH.Dictionary (PrimState IO) ks Int vs Int)
mkSpec
:: forall ks vs. (HashTableTest ks vs)
=> Proxy ks -> Proxy vs -> Spec
mkSpec ksp vsp = describe (specDescription ksp vsp) $
modifyMaxSuccess (const 1000) $ do
it "lookup for inserted value at specific index returns value" $
property prop_insertLookup
it "lookup for inserted value at specific index returns nothing" $
property prop_insertLookupNothing
it "lookup for inserted value at specific index throws error" $
property prop_insertLookupError
it "lookup for updated value at specific index returns updated value" $
property prop_insertUpdateLookup
it "lookup for deleted value at specific index returns nothing" $
property prop_insertDeleteLookupNothing
it "lookup for deleted value at specific index throws error" $
property prop_insertDeleteLookupError
it "table size increases when multiple elements added" $
property prop_insertMultipleElements
it "lookup for inserted value with hash collision returns value" $
property prop_insertLookupHashCollisions
it "fromList . toList === id" $ property prop_fromListToList
it "deleted entries are not present in key-value list after deleting from hashtable" $
property prop_insertDeleteKeysSize
it "new table is null" $ property prop_newIsNull
it "non-empty table is not null" $ property prop_fromListIsNotNull
it "inserted key is table member" $ property prop_isMember
it "deleted key is not a member" $ property prop_isNotMember
it "when altering is nothing - key deleted from table" $ property prop_alterDelete
it "when altering is just a result - key updated with result" $ property prop_alterUpdate
it "intersection + symmetric difference of two tables is equal to union of two tables" $ property prop_union
where
prop_insertLookup
:: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertLookup (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
v <- testAt ht x
v `shouldBe` y
prop_insertLookupNothing :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertLookupNothing (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht (x + 1) y
v <- testAt' ht x
v `shouldBe` Nothing
prop_insertLookupError :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertLookupError (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht (x + 1) y
testAt ht x `shouldThrow` errorCall "KeyNotFoundException!"
prop_insertUpdateLookup :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertUpdateLookup (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
testInsert ht x (y + 1)
v <- testAt ht x
v `shouldBe` (y + 1)
prop_insertDeleteLookupNothing :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertDeleteLookupNothing (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
testDelete ht x
v <- testAt' ht x
v `shouldBe` Nothing
prop_insertDeleteLookupError :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_insertDeleteLookupError (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht 0 1
testDelete ht 0
testAt ht 0 `shouldThrow` errorCall "KeyNotFoundException!"
prop_insertMultipleElements
:: HashTableTest ks vs
=> (NonNegative Int) -> Property
prop_insertMultipleElements (NonNegative n) = forAll (listN n) $ \xs -> do
ht <- testInit (Proxy @ks) (Proxy @vs) 2
mapM_ (uncurry (testInsert ht)) xs
htl <- testLength ht
htl `shouldBe` (length . Set.toList . Set.fromList) (fst <$> xs)
prop_insertLookupHashCollisions
:: HashTableTest ks vs => (AlwaysCollide, Int) -> (AlwaysCollide, Int) -> IO ()
prop_insertLookupHashCollisions (x1, y1) (x2, y2) = do
ht <- testInitCollide (Proxy @ks) (Proxy @vs) 10
let x2' = if x1 /= x2 then x2 else x2 + 1
testInsertCollide ht x1 y1
testInsertCollide ht x2' y2
v <- testAtCollide ht x1
v `shouldBe` y1
prop_fromListToList :: NonNegative Int -> Property
prop_fromListToList (NonNegative n) = forAll (shuffledListN n) $ \(xs, ys) -> do
ht <- testFromList (Proxy @ks) (Proxy @vs) xs
xs' <- testToList ht
L.sort xs' `shouldBe` L.sort ys
prop_insertDeleteKeysSize :: NonNegative Int -> Property
prop_insertDeleteKeysSize (NonNegative n) = forAll (listsForRemoveN n) go
where
go (insertData, deleteData) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 2
mapM_ (uncurry (testInsert ht)) insertData
mapM_ (testDelete ht) deleteData
kvs <- testToList ht
L.length insertData - L.length deleteData `shouldBe` L.length kvs
prop_newIsNull :: IO ()
prop_newIsNull = do
ht <- testInit (Proxy @ks) (Proxy @vs) 2
result <- testNull ht
result `shouldBe` True
prop_fromListIsNotNull :: Positive Int -> Property
prop_fromListIsNotNull (Positive n) = forAll (listN n) $ \xs -> do
ht <- testFromList (Proxy @ks) (Proxy @vs) xs
result <- testNull ht
result `shouldBe` False
prop_isMember :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_isMember (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
v <- testMember ht x
v `shouldBe` True
prop_isNotMember :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_isNotMember (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
testDelete ht x
v <- testMember ht x
v `shouldBe` False
prop_alterDelete :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_alterDelete (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
testAlter ht (const Nothing) x
v <- testMember ht x
v `shouldBe` False
prop_alterUpdate :: HashTableTest ks vs => (Int, Int) -> IO ()
prop_alterUpdate (x, y) = do
ht <- testInit (Proxy @ks) (Proxy @vs) 10
testInsert ht x y
testAlter ht (fmap negate) x
v <- testAt ht x
v `shouldBe` (negate y)
prop_union :: Positive Int -> Property
prop_union (Positive n) = forAll (twoListsN n) $ \(xs, ys) -> do
ht1 <- testFromList (Proxy @ks) (Proxy @vs) xs
ht2 <- testFromList (Proxy @ks) (Proxy @vs) ys
u1 <- testUnion ht1 ht2
d1 <- testDifference ht1 ht2
d2 <- testDifference ht2 ht1
i <- testIntersection ht1 ht2
res <- do
u2 <- testUnion d1 d2
testUnion i u2
resultList <- testToList res
unionList <- testToList u1
Set.fromList resultList `shouldBe` Set.fromList unionList
instance HashTableTest M.MVector M.MVector where
specDescription _ _ = "Data.Vector.HashTables.Mutable keys and values"
testInit _ _ n = VH.initialize n
testInitCollide _ _ n = VH.initialize n
testInsert = VH.insert
testAt = VH.at
testAt' = VH.at'
testDelete = VH.delete
testInsertCollide = VH.insert
testAtCollide = VH.at
testLength = VH.length
testFromList _ _ = VH.fromList
testToList = VH.toList
testNull = VH.null
testMember = VH.member
testAlter = VH.alter
testUnion = VH.union
testDifference = VH.difference
testIntersection = VH.intersection
mutableSpec :: Spec
mutableSpec = mkSpec (Proxy :: Proxy M.MVector) (Proxy :: Proxy M.MVector)
instance HashTableTest SM.MVector SM.MVector where
specDescription _ _ = "Data.Vector.HashTables.Storable.Mutable keys and values"
testInit _ _ n = VH.initialize n
testInitCollide _ _ n = VH.initialize n
testInsert = VH.insert
testAt = VH.at
testAt' = VH.at'
testDelete = VH.delete
testInsertCollide = VH.insert
testAtCollide = VH.at
testLength = VH.length
testFromList _ _ = VH.fromList
testToList = VH.toList
testNull = VH.null
testMember = VH.member
testAlter = VH.alter
testUnion = VH.union
testDifference = VH.difference
testIntersection = VH.intersection
storableMutableSpec :: Spec
storableMutableSpec = mkSpec (Proxy @SM.MVector) (Proxy @SM.MVector)
instance HashTableTest SM.MVector M.MVector where
specDescription _ _ = "Data.Vector.HashTables.Mutable keys and Data.Vector.HashTables.Storable.Mutable values"
testInit _ _ n = VH.initialize n
testInitCollide _ _ n = VH.initialize n
testInsert = VH.insert
testAt = VH.at
testAt' = VH.at'
testDelete = VH.delete
testInsertCollide = VH.insert
testAtCollide = VH.at
testLength = VH.length
testFromList _ _ = VH.fromList
testToList = VH.toList
testNull = VH.null
testMember = VH.member
testAlter = VH.alter
testUnion = VH.union
testDifference = VH.difference
testIntersection = VH.intersection
storableKeysSpec :: Spec
storableKeysSpec = mkSpec (Proxy @SM.MVector) (Proxy @M.MVector)
instance HashTableTest M.MVector UM.MVector where
specDescription _ _ = "Data.Vector.HashTables.Mutable keys and Data.Vector.HashTables.Unboxed.Mutable values"
testInit _ _ n = VH.initialize n
testInitCollide _ _ n = VH.initialize n
testInsert = VH.insert
testAt = VH.at
testAt' = VH.at'
testDelete = VH.delete
testInsertCollide = VH.insert
testAtCollide = VH.at
testLength = VH.length
testFromList _ _ = VH.fromList
testToList = VH.toList
testNull = VH.null
testMember = VH.member
testAlter = VH.alter
testUnion = VH.union
testDifference = VH.difference
testIntersection = VH.intersection
unboxedKeysSpec :: Spec
unboxedKeysSpec = mkSpec (Proxy @M.MVector) (Proxy @UM.MVector)