packages feed

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` ()