packages feed

heph-sparse-set-0.1.0.0: test/Data/SparseSet/Generic/Internal/GrowVecSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

module Data.SparseSet.Generic.Internal.GrowVecSpec where

import Control.Monad (foldM, forM)
import Data.Vector.Unboxed.Mutable qualified as VM
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Tasty.HUnit

import Data.SparseSet.Generic.Mutable.Internal.GrowVec qualified as GV

type TestComponent = Int

unit_withCapacity_is_empty :: Assertion
unit_withCapacity_is_empty = do
  vec <- GV.withCapacity @TestComponent @VM.MVector 128
  GV.length vec @?= 0

unit_withCapacity_valid_capacity :: Assertion
unit_withCapacity_valid_capacity = do
  vec <- GV.withCapacity @TestComponent @VM.MVector 0
  vec' <- GV.withCapacity @TestComponent @VM.MVector 1
  assertBool "capacity must be greater than 2" (GV.capacity vec >= 2 && GV.capacity vec' >= 2)

-- HUnit Tests for specific scenarios
unit_new_is_empty :: Assertion
unit_new_is_empty = do
  vec <- GV.new @TestComponent @VM.MVector
  GV.length vec @?= 0

unit_snoc_increases_length :: Assertion
unit_snoc_increases_length = do
  vec <- GV.new @TestComponent @VM.MVector
  vec' <- GV.snoc vec 100
  GV.length vec' @?= 1

unit_read_after_snoc :: Assertion
unit_read_after_snoc = do
  vec <- GV.new @TestComponent @VM.MVector
  vec' <- GV.snoc vec 100
  mVal <- GV.readMaybe vec' 0
  mVal @?= Just 100

unit_swapRemove_middle :: Assertion
unit_swapRemove_middle = do
  vec <- GV.new @TestComponent @VM.MVector
  vec1 <- GV.snoc vec 10
  vec2 <- GV.snoc vec1 20
  vec3 <- GV.snoc vec2 30 -- vec = [10, 20, 30]
  (removed, vec4) <- GV.unsafeSwapRemove vec3 1 -- remove 20
  -- now vec should be [10, 30]
  val0 <- GV.unsafeRead vec4 0
  val1 <- GV.unsafeRead vec4 1
  mVal2 <- GV.readMaybe vec4 2
  removed @?= 20
  GV.length vec4 @?= 2
  val0 @?= 10
  val1 @?= 30
  mVal2 @?= Nothing

unit_compact_preserves_elements :: Assertion
unit_compact_preserves_elements = do
  vec1 <- GV.withCapacity @TestComponent @VM.MVector 100
  vec2 <- GV.snoc vec1 10
  vec3 <- GV.snoc vec2 20
  vec4 <- GV.compact vec3
  GV.length vec4 @?= 2
  val0 <- GV.unsafeRead vec4 0
  val1 <- GV.unsafeRead vec4 1
  val0 @?= 10
  val1 @?= 20

unit_maximum_empty :: Assertion
unit_maximum_empty = do
  vec <- GV.new @TestComponent @VM.MVector
  mMax <- GV.maximum vec
  mMax @?= Nothing

unit_maximum_single :: Assertion
unit_maximum_single = do
  vec <- GV.new @TestComponent @VM.MVector
  vec' <- GV.snoc vec 42
  mMax <- GV.maximum vec'
  mMax @?= Just 42

unit_maximum_multiple :: Assertion
unit_maximum_multiple = do
  vec <- GV.new @TestComponent @VM.MVector
  vec1 <- GV.snoc vec (-10)
  vec2 <- GV.snoc vec1 30
  vec3 <- GV.snoc vec2 5
  mMax <- GV.maximum vec3
  mMax @?= Just 30

unit_clear :: Assertion
unit_clear = do
  vec <- GV.new @TestComponent @VM.MVector
  vec1 <- GV.snoc vec 10
  vec2 <- GV.snoc vec1 20
  let vec3 = GV.cleared vec2
  GV.length vec2 @?= 2 -- Original is unchanged
  GV.length vec3 @?= 0 -- New is empty

-- Hedgehog property-based tests
data GrowVecOp
  = OpSnoc TestComponent
  | OpWrite Int TestComponent
  | OpSwapRemove Int
  | OpClear
  deriving (Show, Eq)

genOp :: Int -> Gen GrowVecOp
genOp currentLen =
  Gen.frequency $
    [ (10, OpSnoc <$> Gen.int (Range.linear 0 1000))
    ]
      <> [ (5, OpWrite <$> Gen.int (Range.linear 0 (currentLen - 1)) <*> Gen.int (Range.linear 0 1000))
         | currentLen > 0
         ]
      <> [ (3, OpSwapRemove <$> Gen.int (Range.linear 0 (currentLen - 1))) | currentLen > 0
         ]
      <> [(1, pure OpClear) | currentLen > 0]

genOps :: Gen [GrowVecOp]
genOps = do
  size <- Gen.int (Range.linear 0 100)
  go size []
 where
  go :: Int -> [GrowVecOp] -> Gen [GrowVecOp]
  go n acc | n <= 0 = pure (reverse acc)
  go n acc = do
    let currentModel = applyOpsToModel (reverse acc) []
    op <- genOp (length currentModel)
    go (n - 1) (op : acc)

-- Pure list model of the GrowVec operations
applyOpToModel :: GrowVecOp -> [TestComponent] -> [TestComponent]
applyOpToModel (OpSnoc c) xs = xs ++ [c]
applyOpToModel (OpWrite idx val) xs = take idx xs ++ [val] ++ drop (idx + 1) xs
applyOpToModel (OpSwapRemove idx) xs
  | idx == lastIdx = take idx xs
  | otherwise = take idx xs ++ [last xs] ++ drop (idx + 1) (take lastIdx xs)
 where
  lastIdx = length xs - 1
applyOpToModel OpClear _ = []

applyOpsToModel :: [GrowVecOp] -> [TestComponent] -> [TestComponent]
applyOpsToModel ops model = foldl (flip applyOpToModel) model ops

hprop_growvec_model :: Property
hprop_growvec_model = property $ do
  ops <- forAll genOps
  let finalModel = applyOpsToModel ops []
  vec <- GV.new @TestComponent @VM.MVector
  vec' <-
    foldM
      ( \v op -> case op of
          OpSnoc c -> GV.snoc v c
          OpWrite idx val -> GV.unsafeWrite v idx val >> pure v
          OpSwapRemove idx -> snd <$> GV.unsafeSwapRemove v idx
          OpClear -> pure $ GV.cleared v
      )
      vec
      ops

  let len = GV.length vec'
  contents <- forM [0 .. len - 1] (GV.unsafeRead vec')

  len === length finalModel
  contents === finalModel