{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-} {- HLINT ignore "Redundant bracket" -} {- HLINT ignore "Use any" -} {- HLINT ignore "Use null" -} -- | -- Copyright: © 2022–2024 Jonathan Knowles -- License: Apache-2.0 -- module Examples.MultiMapSpec where import Prelude import Data.Function ( (&) ) import Data.Maybe ( isJust, isNothing ) import Data.Proxy ( Proxy (..) ) import Data.Set ( Set ) import Data.Typeable ( Typeable, typeRep ) import Examples.MultiMap.Class ( MultiMap ) import Examples.MultiMap.Instances.MultiMap1 ( MultiMap1 ) import Examples.MultiMap.Instances.MultiMap2 ( MultiMap2 ) import Examples.MultiMap.Instances.MultiMap3 ( MultiMap3 ) import Examples.MultiMap.Instances.MultiMap4 ( MultiMap4 ) import Test.Hspec ( Spec, describe, it ) import Test.QuickCheck ( Arbitrary (..) , Property , Testable , checkCoverage , counterexample , cover , (.||.) , (===) ) import Test.QuickCheck.Instances.Natural () import Test.QuickCheck.Instances.Text () import qualified Data.Foldable as F import qualified Data.Set as Set import qualified Examples.MultiMap.Class as M import qualified Test.QuickCheck as QC spec :: Spec spec = do -- Uncomment the following line to see property test failures for an -- unlawful implementation of 'MultiMap': -- -- specFor (Proxy @(MultiMap1 Int Int)) specFor (Proxy @(MultiMap2 Int Int)) specFor (Proxy @(MultiMap3 Int Int)) specFor (Proxy @(MultiMap4 Int Int)) type Test m k v = ( Arbitrary k , Arbitrary v , MultiMap m k v , Show (m k v) , Show k , Show v , Typeable m , Typeable k , Typeable v ) specFor :: forall m k v. Test m k v => Proxy (m k v) -> Spec specFor multimapType = do let description = show (typeRep multimapType) let property :: Testable t => t -> Property property = checkCoverage . QC.property describe description $ do describe "Validity properties" $ do it "prop_fromList_valid" $ prop_fromList_valid @m @k @v & property it "prop_update_valid" $ prop_update_valid @m @k @v & property it "prop_insert_valid" $ prop_insert_valid @m @k @v & property it "prop_remove_valid" $ prop_remove_valid @m @k @v & property it "prop_union_valid" $ prop_union_valid @m @k @v & property it "prop_intersection_valid" $ prop_intersection_valid @m @k @v & property describe "General properties" $ do it "prop_fromList_filter" $ prop_fromList_filter @m @k @v & property it "prop_toList_filter" $ prop_toList_filter @m @k @v & property it "prop_empty_fromList" $ prop_empty_fromList @m @k @v & property it "prop_lookup_filter_fold" $ prop_lookup_filter_fold @m @k @v & property it "prop_null_lookup" $ prop_null_lookup @m @k @v & property it "prop_nonNull_lookup" $ prop_nonNull_lookup @m @k @v & property it "prop_nonNullKey_lookup" $ prop_nonNullKey_lookup @m @k @v & property it "prop_nonNullKeys_nonNullKey" $ prop_nonNullKeys_nonNullKey @m @k @v & property it "prop_nonNullCount_nonNullKeys" $ prop_nonNullCount_nonNullKeys @m @k @v & property it "prop_isSubmapOf_lookup" $ prop_isSubmapOf_lookup @m @k @v & property it "prop_update_lookup" $ prop_update_lookup @m @k @v & property it "prop_insert_lookup" $ prop_insert_lookup @m @k @v & property it "prop_remove_lookup" $ prop_remove_lookup @m @k @v & property it "prop_union_idempotence" $ prop_union_idempotence @m @k @v & property it "prop_union_identity_left" $ prop_union_identity_left @m @k @v & property it "prop_union_identity_right" $ prop_union_identity_right @m @k @v & property it "prop_union_commutativity" $ prop_union_commutativity @m @k @v & property it "prop_union_associativity" $ prop_union_associativity @m @k @v & property it "prop_union_containment_left" $ prop_union_containment_left @m @k @v & property it "prop_union_containment_right" $ prop_union_containment_right @m @k @v & property it "prop_union_distributivity" $ prop_union_distributivity @m @k @v & property it "prop_intersection_idempotence" $ prop_intersection_idempotence @m @k @v & property it "prop_intersection_identity_left" $ prop_intersection_identity_left @m @k @v & property it "prop_intersection_identity_right" $ prop_intersection_identity_right @m @k @v & property it "prop_intersection_commutativity" $ prop_intersection_commutativity @m @k @v & property it "prop_intersection_associativity" $ prop_intersection_associativity @m @k @v & property it "prop_intersection_containment_left" $ prop_intersection_containment_left @m @k @v & property it "prop_intersection_containment_right" $ prop_intersection_containment_right @m @k @v & property it "prop_intersection_distributivity" $ prop_intersection_distributivity @m @k @v & property -------------------------------------------------------------------------------- -- Validity properties -------------------------------------------------------------------------------- -- A multimap is valid if (and only if): -- -- - all keys included in 'nonNullKeys' are associated with non-empty sets. -- - all keys included in 'toList' are associated with non-empty sets. prop_valid :: Test m k v => m k v -> Property prop_valid m = QC.conjoin [ counterexample "prop_valid_nonNullKeys" (prop_valid_nonNullKeys) , counterexample "prop_valid_toList" (prop_valid_toList) ] & cover 1 (M.null m) "M.null m" & cover 1 (M.nonNull m) "M.nonNull m" where prop_valid_nonNullKeys = all (\k -> M.lookup k m /= Set.empty) (M.nonNullKeys m) prop_valid_toList = all (\(_, v) -> v /= Set.empty) (M.toList m) -------------------------------------------------------------------------------- -- Validity of operations that produce multimaps -------------------------------------------------------------------------------- prop_fromList_valid :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_fromList_valid kvs = prop_valid @m @k @v (M.fromList kvs) prop_update_valid :: forall m k v. Test m k v => k -> Set v -> [(k, Set v)] -> Property prop_update_valid k vs kvs = prop_valid @m @k @v (M.update k vs (M.fromList kvs)) prop_insert_valid :: forall m k v. Test m k v => k -> Set v -> [(k, Set v)] -> Property prop_insert_valid k vs kvs = prop_valid @m @k @v (M.insert k vs (M.fromList kvs)) prop_remove_valid :: forall m k v. Test m k v => k -> Set v -> [(k, Set v)] -> Property prop_remove_valid k vs kvs = prop_valid @m @k @v (M.remove k vs (M.fromList kvs)) prop_union_valid :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_union_valid kvs1 kvs2 = prop_valid @m @k @v (M.union (M.fromList kvs1) (M.fromList kvs2)) prop_intersection_valid :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_valid kvs1 kvs2 = prop_valid @m @k @v (M.intersection (M.fromList kvs1) (M.fromList kvs2)) -------------------------------------------------------------------------------- -- General properties -------------------------------------------------------------------------------- prop_fromList_filter :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_fromList_filter kvs = M.fromList @m @k @v kvs === M.fromList (filter ((/= Set.empty) . snd) kvs) prop_toList_filter :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_toList_filter kvs = M.toList m === filter ((/= Set.empty) . snd) (M.toList m) where m :: m k v m = M.fromList kvs prop_empty_fromList :: forall m k v. Test m k v => Property prop_empty_fromList = M.empty @m @k @v === M.fromList [] prop_lookup_filter_fold :: forall m k v. Test m k v => k -> [(k, Set v)] -> Property prop_lookup_filter_fold k kvs = M.lookup k m === F.foldMap snd (filter ((== k) . fst) kvs) & cover 10 (isJust (lookup k kvs)) "isJust (lookup k kvs)" & cover 10 (isNothing (lookup k kvs)) "isNothing (lookup k kvs)" where m :: m k v m = M.fromList kvs prop_null_lookup :: forall m k v. Test m k v => k -> [(k, Set v)] -> Property prop_null_lookup k kvs = M.null m ==> M.lookup k m == Set.empty & cover 2 (M.lookup k m == Set.empty && M.null m) "M.lookup k m == Set.empty && M.null m" & cover 2 (M.lookup k m == Set.empty && M.nonNull m) "M.lookup k m == Set.empty && M.nonNull m" & cover 2 (M.lookup k m /= Set.empty && M.nonNull m) "M.lookup k m /= Set.empty && M.nonNull m" where m :: m k v m = M.fromList kvs prop_nonNull_lookup :: forall m k v. Test m k v => k -> [(k, Set v)] -> Property prop_nonNull_lookup k kvs = M.lookup k m /= Set.empty ==> M.nonNull m & cover 2 (M.lookup k m == Set.empty && M.null m) "M.lookup k m == Set.empty && M.null m" & cover 2 (M.lookup k m == Set.empty && M.nonNull m) "M.lookup k m == Set.empty && M.nonNull m" & cover 2 (M.lookup k m /= Set.empty && M.nonNull m) "M.lookup k m /= Set.empty && M.nonNull m" where m :: m k v m = M.fromList kvs prop_nonNullKey_lookup :: forall m k v. Test m k v => k -> [(k, Set v)] -> Property prop_nonNullKey_lookup k kvs = M.nonNullKey k m === (M.lookup k m /= Set.empty) & cover 2 (M.lookup k m == Set.empty && M.null m) "M.lookup k m == Set.empty && M.null m" & cover 2 (M.lookup k m == Set.empty && M.nonNull m) "M.lookup k m == Set.empty && M.nonNull m" & cover 2 (M.lookup k m /= Set.empty && M.nonNull m) "M.lookup k m /= Set.empty && M.nonNull m" where m :: m k v m = M.fromList kvs prop_nonNullKeys_nonNullKey :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_nonNullKeys_nonNullKey kvs = QC.property $ all (`M.nonNullKey` m) (M.nonNullKeys m) & cover 2 (M.null m) "M.null m" & cover 2 (M.nonNull m) "M.nonNull m" where m :: m k v m = M.fromList kvs prop_nonNullCount_nonNullKeys :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_nonNullCount_nonNullKeys kvs = M.nonNullCount m === Set.size (M.nonNullKeys m) & cover 1 (M.nonNullCount m == 0) "M.nonNullCount m == 0" & cover 1 (M.nonNullCount m == 1) "M.nonNullCount m == 1" & cover 1 (M.nonNullCount m == 2) "M.nonNullCount m == 2" & cover 1 (M.nonNullCount m >= 3) "M.nonNullCount m >= 3" where m :: m k v m = M.fromList kvs prop_isSubmapOf_lookup :: forall m k v. Test m k v => k -> [(k, Set v)] -> [(k, Set v)] -> Property prop_isSubmapOf_lookup k kvs1 kvs2 = m1 `M.isSubmapOf` m2 ==> M.lookup k m1 `Set.isSubsetOf` M.lookup k m2 & cover 1 (m1 `M.isSubmapOf` m2) "m1 `M.isSubmapOf` m2" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_update_lookup :: forall m k v. Test m k v => k -> k -> Set v -> [(k, Set v)] -> Property prop_update_lookup k1 k2 vs kvs = M.lookup k1 (M.update k2 vs m) === (if k1 == k2 then vs else M.lookup k1 m) & cover 1 (k1 == k2) "k1 == k2" & cover 10 (k1 /= k2) "k1 /= k2" where m :: m k v m = M.fromList kvs prop_insert_lookup :: forall m k v. Test m k v => k -> k -> Set v -> [(k, Set v)] -> Property prop_insert_lookup k1 k2 vs kvs = M.lookup k1 (M.insert k2 vs m) === (if k1 == k2 then M.lookup k1 m `Set.union` vs else M.lookup k1 m) & cover 1 (k1 == k2) "k1 == k2" & cover 10 (k1 /= k2) "k1 /= k2" where m :: m k v m = M.fromList kvs prop_remove_lookup :: forall m k v. Test m k v => k -> k -> Set v -> [(k, Set v)] -> Property prop_remove_lookup k1 k2 vs kvs = M.lookup k1 (M.remove k2 vs m) === (if k1 == k2 then M.lookup k1 m `Set.difference` vs else M.lookup k1 m) & cover 1 (k1 == k2) "k1 == k2" & cover 10 (k1 /= k2) "k1 /= k2" where m :: m k v m = M.fromList kvs prop_union_idempotence :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_union_idempotence kvs = M.union m m === m where m :: m k v m = M.fromList kvs prop_union_identity_left :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_union_identity_left kvs = M.union m M.empty === m where m :: m k v m = M.fromList kvs prop_union_identity_right :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_union_identity_right kvs = M.union M.empty m === m where m :: m k v m = M.fromList kvs prop_union_commutativity :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_union_commutativity kvs1 kvs2 = M.union m1 m2 === M.union m2 m1 where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_union_associativity :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> [(k, Set v)] -> Property prop_union_associativity kvs1 kvs2 kvs3 = M.union m1 (M.union m2 m3) === M.union (M.union m1 m2) m3 where m1, m2, m3 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 m3 = M.fromList kvs3 prop_union_containment_left :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_union_containment_left kvs1 kvs2 = QC.property $ m1 `M.isSubmapOf` M.union m1 m2 where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_union_containment_right :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_union_containment_right kvs1 kvs2 = QC.property $ m2 `M.isSubmapOf` M.union m1 m2 where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_union_distributivity :: forall m k v. Test m k v => k -> [(k, Set v)] -> [(k, Set v)] -> Property prop_union_distributivity k kvs1 kvs2 = M.lookup k (M.union m1 m2) === Set.union (M.lookup k m1) (M.lookup k m2) & cover 1 (M.nonNullKey k (M.union m1 m2)) "M.nonNullKey k (M.union m1 m2)" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_intersection_idempotence :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_intersection_idempotence kvs = M.intersection m m === m where m :: m k v m = M.fromList kvs prop_intersection_identity_left :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_intersection_identity_left kvs = M.intersection m M.empty === M.empty where m :: m k v m = M.fromList kvs prop_intersection_identity_right :: forall m k v. Test m k v => [(k, Set v)] -> Property prop_intersection_identity_right kvs = M.intersection M.empty m === M.empty where m :: m k v m = M.fromList kvs prop_intersection_commutativity :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_commutativity kvs1 kvs2 = M.intersection m1 m2 === M.intersection m2 m1 & cover 1 (M.nonNull (M.intersection m1 m2)) "M.nonNull (M.intersection m1 m2)" & cover 1 (M.nonNull (M.intersection m2 m1)) "M.nonNull (M.intersection m2 m1)" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_intersection_associativity :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_associativity kvs1 kvs2 kvs3 = M.intersection m1 (M.intersection m2 m3) === M.intersection (M.intersection m1 m2) m3 & cover 1 (M.nonNull (M.intersection m1 (M.intersection m2 m3))) "M.nonNull (M.intersection m1 (M.intersection m2 m3))" & cover 1 (M.nonNull (M.intersection (M.intersection m1 m2) m3)) "M.nonNull (M.intersection (M.intersection m1 m2) m3)" where m1, m2, m3 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 m3 = M.fromList kvs3 prop_intersection_containment_left :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_containment_left kvs1 kvs2 = QC.property $ M.intersection m1 m2 `M.isSubmapOf` m1 & cover 1 (M.nonNull (M.intersection m1 m2)) "M.nonNull (M.intersection m1 m2)" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_intersection_containment_right :: forall m k v. Test m k v => [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_containment_right kvs1 kvs2 = QC.property $ M.intersection m1 m2 `M.isSubmapOf` m2 & cover 1 (M.nonNull (M.intersection m1 m2)) "M.nonNull (M.intersection m1 m2)" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 prop_intersection_distributivity :: forall m k v. Test m k v => k -> [(k, Set v)] -> [(k, Set v)] -> Property prop_intersection_distributivity k kvs1 kvs2 = M.lookup k (M.intersection m1 m2) === Set.intersection (M.lookup k m1) (M.lookup k m2) & cover 1 (M.nonNullKey k (M.intersection m1 m2)) "M.nonNullKey k (M.intersection m1 m2)" where m1, m2 :: m k v m1 = M.fromList kvs1 m2 = M.fromList kvs2 -------------------------------------------------------------------------------- -- Utilities -------------------------------------------------------------------------------- infixr 3 ==> (==>) :: Bool -> Bool -> Property a ==> b = not a .||. b _preventRedundantImportErrors :: () _preventRedundantImportErrors = () where _multiMap1 :: MultiMap1 () () _multiMap1 = M.empty