packages feed

croaring-ffi-1.0: test/Test/Bitset.hs

module Test.Bitset (tests) where

import Control.Exception (try)
import Control.Monad (forM, forM_)
import Data.Bitmap.Roaring.FFI.Bitset (Bitset (..))
import Data.Bitmap.Roaring.FFI.Bitset qualified as Bitset
import Data.Bitmap.Roaring.FFI.NullHandle (NullHandle (..))
import Data.IORef (readIORef)
import Data.Set qualified as Set
import Data.Word (Word64)
import Foreign.C.Types (CSize)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Marshal.Array (allocaArray, peekArray)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek, poke)
import Hedgehog (Gen, Property, assert, evalIO, forAll, (===))
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Bitmap (collect, visiting)
import Test.Property (Test, returns)
import Test.Property qualified as Property
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog (testProperty)

tests :: TestTree
tests =
  testGroup
    "Bitset"
    [ testProperty "set bits and queries" queries
    , testProperty "set algebra" setAlgebra
    , testProperty "shifts" shifts
    , testProperty "buffer fields" fields
    , testProperty "forEach stops when the callback says so" forEachStop
    , testProperty "resize, trim and fill" sizing
    , testProperty "with frees the bitset and rejects NULL" scoped
    , testProperty "forEachWith visits, stops and rethrows" callbackHelpers
    , testProperty "in-place operations with the same bitset twice" selfAliasing
    ]

genBits :: CSize -> Gen [CSize]
genBits limit = Gen.list (Range.linear 0 200) . fmap fromIntegral . Gen.word64 . Range.constant 0 $ fromIntegral limit

created :: IO Bitset -> Test Bitset
created = Property.owned (Bitset nullPtr) Bitset.free

fromBits :: [CSize] -> Test Bitset
fromBits bits = do
  bitset <- created Bitset.create
  evalIO . forM_ bits $ Bitset.set bitset
  pure bitset

toList :: Bitset -> IO [CSize]
toList bitset =
  allocaArray 5 $ \buffer ->
    alloca $ \start -> do
      poke start 0
      let loop = do
            count <- fromIntegral <$> Bitset.nextSetBits bitset buffer 5 start
            found <- peekArray count buffer
            if count == 0 then
              pure []
            else do
              peek start >>= poke start . (+ 1)
              (found <>) <$> loop
      loop

queries :: Property
queries =
  Property.generated $ do
    bits <- forAll $ genBits 5000
    probes <- forAll $ genBits 6000
    bitset <- fromBits bits
    let
      model = Set.fromList bits
      next probe =
        alloca $ \index -> do
          poke index probe
          present <- Bitset.nextSetBit bitset index
          if present then Just <$> peek index else pure Nothing
    toList bitset `returns` Set.toAscList model
    Bitset.count bitset `returns` fromIntegral (Set.size model)
    Bitset.empty bitset `returns` Set.null model
    Bitset.minimum bitset `returns` maybe maxBound fst (Set.minView model)
    Bitset.maximum bitset `returns` maybe 0 fst (Set.maxView model)
    forM probes (Bitset.get bitset) `returns` map (`Set.member` model) probes
    forM probes next `returns` map (`Set.lookupGE` model) probes

setAlgebra :: Property
setAlgebra =
  Property.generated $ do
    leftBits <- forAll $ genBits 3000
    rightBits <- forAll $ genBits 3000
    left <- fromBits leftBits
    right <- fromBits rightBits
    let
      leftModel = Set.fromList leftBits
      rightModel = Set.fromList rightBits
      union = Set.union leftModel rightModel
      intersection = Set.intersection leftModel rightModel
      difference = Set.difference leftModel rightModel
      symmetric = union `Set.difference` intersection
      applied operation = do
        copy <- fromBits leftBits
        _ <- evalIO $ operation copy right
        evalIO $ toList copy
    Bitset.unionCount left right `returns` fromIntegral (Set.size union)
    Bitset.intersectionCount left right `returns` fromIntegral (Set.size intersection)
    Bitset.differenceCount left right `returns` fromIntegral (Set.size difference)
    Bitset.symmetricDifferenceCount left right `returns` fromIntegral (Set.size symmetric)
    Bitset.disjoint left right `returns` Set.disjoint leftModel rightModel
    Bitset.intersect left right `returns` not (Set.disjoint leftModel rightModel)
    Bitset.containsAll left right `returns` Set.isSubsetOf rightModel leftModel
    applied Bitset.inplaceUnion >>= (=== Set.toAscList union)
    applied Bitset.inplaceIntersection >>= (=== Set.toAscList intersection)
    applied Bitset.inplaceDifference >>= (=== Set.toAscList difference)
    applied Bitset.inplaceSymmetricDifference >>= (=== Set.toAscList symmetric)
    copied <- created $ Bitset.copy left
    toList copied `returns` Set.toAscList leftModel

shifts :: Property
shifts =
  Property.generated $ do
    bits <- forAll $ genBits 3000
    shift <- forAll . fmap fromIntegral . Gen.word64 $ Range.linear 0 200
    let ascending = Set.toAscList $ Set.fromList bits
    left <- fromBits bits
    evalIO $ Bitset.shiftLeft left shift
    toList left `returns` map (+ shift) ascending
    right <- fromBits bits
    evalIO $ Bitset.shiftRight right shift
    toList right `returns` [bit - shift | bit <- ascending, bit >= shift]

fields :: Property
fields =
  Property.example $ do
    bitset <- fromBits [0, 63, 64, 200]
    used <- evalIO $ Bitset.sizeInWords bitset
    Bitset.arraySize bitset `returns` used
    Bitset.sizeInBits bitset `returns` (used * 64)
    Bitset.sizeInBytes bitset `returns` (used * 8)
    capacity <- evalIO $ Bitset.capacity bitset
    assert $ capacity >= used
    take 4 <$> (Bitset.array bitset >>= peekArray (fromIntegral used)) `returns` [2 ^ (63 :: Int) + 1, 1, 0, 2 ^ (200 - 192 :: Int) :: Word64]

forEachStop :: Property
forEachStop =
  Property.example $ do
    bitset <- fromBits [1, 5, 9, 700]
    (seen, callback) <- collect Bitset.wrapCallback (< 9)
    Bitset.forEach bitset callback nullPtr `returns` False
    reverse <$> readIORef seen `returns` [1, 5, 9]

sizing :: Property
sizing =
  Property.example $ do
    bitset <- created $ Bitset.createWithCapacity 128
    Bitset.sizeInWords bitset `returns` 2
    Bitset.count bitset `returns` 0
    evalIO $ Bitset.fill bitset
    Bitset.count bitset `returns` 128
    evalIO $ Bitset.setToValue bitset 3 False
    Bitset.get bitset 3 `returns` False
    Bitset.resize bitset 4 True `returns` True
    Bitset.sizeInWords bitset `returns` 4
    Bitset.count bitset `returns` 127
    Bitset.grow bitset 8 `returns` True
    Bitset.sizeInWords bitset `returns` 8
    evalIO $ Bitset.clear bitset
    Bitset.empty bitset `returns` True
    Bitset.trim bitset `returns` True

scoped :: Property
scoped =
  Property.example $ do
    Bitset.with (Bitset.createWithCapacity 64) (\bitset -> Bitset.set bitset 5 >> Bitset.count bitset) `returns` 1
    outcome <- evalIO . try $ Bitset.with (pure $ Bitset nullPtr) Bitset.count
    outcome === Left (NullHandle "Data.Bitmap.Roaring.FFI.Bitset.with")

callbackHelpers :: Property
callbackHelpers =
  Property.example $ do
    bitset <- fromBits [1 .. 10]
    visiting (Bitset.forEachWith bitset) (const $ pure True) `returns` (Right True, [1 .. 10])
    visiting (Bitset.forEachWith bitset) (pure . (< 3)) `returns` (Right False, [1, 2, 3])
    visiting (Bitset.forEachWith bitset) stopAtThree `returns` (Left $ userError "stop", [1, 2, 3])
  where
    stopAtThree bit = if bit == 3 then ioError $ userError "stop" else pure True

selfAliasing :: Property
selfAliasing =
  Property.generated $ do
    bits <- forAll $ genBits 3000
    let
      expected = Set.toAscList $ Set.fromList bits
      applied operation = do
        bitset <- fromBits bits
        result <- evalIO $ operation bitset bitset
        (,) result <$> evalIO (toList bitset)
    applied Bitset.inplaceUnion >>= (=== (True, expected))
    applied Bitset.inplaceIntersection >>= (=== ((), expected))
    applied Bitset.inplaceDifference >>= (=== ((), []))
    applied Bitset.inplaceSymmetricDifference >>= (=== (True, []))