openapi-hs-5.0.0: test/Data/HashSet/InsOrdSpec.hs
module Data.HashSet.InsOrdSpec where
import Control.DeepSeq (force, rnf)
import Control.DeepSeq qualified as NF
import Control.Exception (evaluate)
import Control.Lens (at, contains, ix, (&), (.~), (?~), (^?))
import Data.Aeson (eitherDecode, encode)
import Data.ByteString.Lazy.Char8 qualified as BSL
import Data.HashSet qualified as HashSet
import Data.HashSet.InsOrd.Compat qualified as InsOrd
import Data.Word (Word8)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import Test.QuickCheck.Function (Fun (..))
data Operation
= FromList [Word8]
| Empty
| Singleton Word8
| Insert Word8 Operation
| Delete Word8 Operation
| Union Operation Operation
| Difference Operation Operation
| Intersection Operation Operation
| Map (Fun Word8 Word8) Operation
| Filter (Fun Word8 Bool) Operation
deriving (Show)
instance Arbitrary Operation where
arbitrary = sized gen
where
terminals =
[ FromList <$> arbitrary,
pure Empty,
Singleton <$> arbitrary
]
gen 0 = oneof terminals
gen n =
oneof $
terminals
<> [ Insert <$> arbitrary <*> smaller,
Delete <$> arbitrary <*> smaller,
Union <$> half <*> half,
Difference <$> half <*> half,
Intersection <$> half <*> half,
Map <$> arbitrary <*> smaller,
Filter <$> arbitrary <*> smaller
]
where
smaller = gen (n - 1)
half = gen (n `div` 2)
evalInsOrd :: Operation -> InsOrd.InsOrdHashSet Word8
evalInsOrd operation = case operation of
FromList values -> InsOrd.fromList values
Empty -> InsOrd.empty
Singleton value -> InsOrd.singleton value
Insert value rest -> InsOrd.insert value (evalInsOrd rest)
Delete value rest -> InsOrd.delete value (evalInsOrd rest)
Union left right -> InsOrd.union (evalInsOrd left) (evalInsOrd right)
Difference left right -> InsOrd.difference (evalInsOrd left) (evalInsOrd right)
Intersection left right -> InsOrd.intersection (evalInsOrd left) (evalInsOrd right)
Map (Fun _ f) rest -> InsOrd.map f (evalInsOrd rest)
Filter (Fun _ predicate) rest -> InsOrd.filter predicate (evalInsOrd rest)
evalHashSet :: Operation -> HashSet.HashSet Word8
evalHashSet operation = case operation of
FromList values -> HashSet.fromList values
Empty -> HashSet.empty
Singleton value -> HashSet.singleton value
Insert value rest -> HashSet.insert value (evalHashSet rest)
Delete value rest -> HashSet.delete value (evalHashSet rest)
Union left right -> HashSet.union (evalHashSet left) (evalHashSet right)
Difference left right -> HashSet.difference (evalHashSet left) (evalHashSet right)
Intersection left right -> HashSet.intersection (evalHashSet left) (evalHashSet right)
Map (Fun _ f) rest -> HashSet.map f (evalHashSet rest)
Filter (Fun _ predicate) rest -> HashSet.filter predicate (evalHashSet rest)
containsUnion :: Operation -> Bool
containsUnion operation = case operation of
FromList _ -> False
Empty -> False
Singleton _ -> False
Insert _ rest -> containsUnion rest
Delete _ rest -> containsUnion rest
Union _ _ -> True
Difference left right -> containsUnion left || containsUnion right
Intersection left right -> containsUnion left || containsUnion right
Map _ rest -> containsUnion rest
Filter _ rest -> containsUnion rest
prop_operationModel :: Operation -> Property
prop_operationModel operation =
let actual = evalInsOrd operation
expected = evalHashSet operation
in conjoin
[ counterexample ("ordered membership: " <> show (InsOrd.toList actual)) $
InsOrd.toHashSet actual === expected,
counterexample ("invalid ordered set: " <> show (InsOrd.toList actual)) $
property (containsUnion operation || InsOrd.valid actual)
]
spec :: Spec
spec = do
describe "operation model" $
prop "matches Data.HashSet and preserves valid outside the union caveat" prop_operationModel
describe "ordered set behavior" $ do
it "keeps the last duplicate at the end" $
InsOrd.toList (InsOrd.fromList ["a", "b", "a"])
`shouldBe` ["b", "a"]
it "moves an existing member to the end when inserted again" $
InsOrd.toList (InsOrd.insert "a" (InsOrd.fromList ["a", "b"]))
`shouldBe` ["b", "a"]
it "uses left-biased union order followed by right-only members" $
InsOrd.toList (InsOrd.union (InsOrd.fromList ["a", "b"]) (InsOrd.fromList ["b", "c"]))
`shouldBe` ["a", "b", "c"]
it "preserves the released union valid caveat and supports normalization" $ do
let combined = InsOrd.union (InsOrd.singleton "a") (InsOrd.singleton "b")
InsOrd.valid combined `shouldBe` False
InsOrd.valid (InsOrd.fromHashSet (InsOrd.toHashSet combined)) `shouldBe` True
it "supports lens ix, at, and contains" $ do
let input = InsOrd.fromList ["a", "b"]
input ^? ix "a" `shouldBe` Just ()
InsOrd.toList (input & at "a" .~ Nothing) `shouldBe` ["b"]
InsOrd.toList (input & at "c" ?~ ()) `shouldBe` ["a", "b", "c"]
InsOrd.toList (input & contains "b" .~ False) `shouldBe` ["a"]
InsOrd.toList (input & contains "c" .~ True) `shouldBe` ["a", "b", "c"]
it "round-trips through Show and Read with order intact" $ do
let input = InsOrd.fromList ['b', 'a']
output = read (show input) :: InsOrd.InsOrdHashSet Char
InsOrd.toList output `shouldBe` InsOrd.toList input
describe "JSON and deep evaluation compatibility" $ do
it "encodes as an insertion-ordered array" $
encode (InsOrd.fromList ["b", "a"])
`shouldBe` BSL.pack "[\"b\",\"a\"]"
it "round-trips order and membership after index gaps" $ do
let input = InsOrd.delete "b" (InsOrd.fromList ["a", "b", "c"])
decoded = eitherDecode (encode input) :: Either String (InsOrd.InsOrdHashSet String)
fmap InsOrd.toList decoded `shouldBe` Right (InsOrd.toList input)
fmap InsOrd.toHashSet decoded `shouldBe` Right (InsOrd.toHashSet input)
it "retains NFData and NFData1 instances" $ do
let input = InsOrd.fromList ["a", "b"]
evaluate (force input) `shouldReturn` input
evaluate (NF.liftRnf rnf input) `shouldReturn` ()