croaring-ffi-1.0: test/Test/Bitmap/Iterator.hs
{-# LANGUAGE OverloadedRecordDot #-}
module Test.Bitmap.Iterator
( tests
, readAll
, modelRanges
) where
import Control.Monad (forM)
import Data.Bitmap.Roaring.FFI.Bitmap (Bitmap)
import Data.Bitmap.Roaring.FFI.Bitmap.Iterator (Iterator (..), Range (..))
import Data.Bitmap.Roaring.FFI.Bitmap.Iterator qualified as Iterator
import Data.Set qualified as Set
import Data.Word (Word32)
import Foreign.Marshal.Array (allocaArray, peekArray)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (Storable, peek, poke)
import Hedgehog (Property, evalIO, forAll)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Bitmap (fromValues, genValues)
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
"Bitmap.Iterator"
[ testProperty "walks forward and backward" walks
, testProperty "reads values in chunks" chunks
, testProperty "reads maximal ranges" ranges
, testProperty "moves to the first value at or above a probe" moves
, testProperty "skips forward and backward" skips
, testProperty "heap iterators copy independently" heap
, testProperty "with and withLast scope an iterator" scoped
, testProperty "range poke and peek agree" rangeStorable
]
placed :: (Bitmap -> Iterator -> IO ()) -> Bitmap -> Test Iterator
placed position bitmap = do
iterator <- Iterator <$> Property.alignedBytes Iterator.size Iterator.alignment
evalIO $ position bitmap iterator
pure iterator
created :: IO Iterator -> Test Iterator
created = Property.owned (Iterator nullPtr) Iterator.free
walk :: (Iterator -> IO Bool) -> Iterator -> IO [Word32]
walk step iterator = do
present <- Iterator.hasValue iterator
if present then do
value <- Iterator.currentValue iterator
_ <- step iterator
(value :) <$> walk step iterator
else
pure []
current :: Iterator -> IO (Maybe Word32)
current iterator = do
present <- Iterator.hasValue iterator
if present then Just <$> Iterator.currentValue iterator else pure Nothing
walks :: Property
walks =
Property.generated $ do
values <- forAll genValues
bitmap <- fromValues values
let expected = Set.toAscList $ Set.fromList values
forward <- placed Iterator.init bitmap
walk Iterator.advance forward `returns` expected
backward <- placed Iterator.initLast bitmap
walk Iterator.previous backward `returns` reverse expected
chunks :: Property
chunks =
Property.generated $ do
values <- forAll genValues
chunk <- forAll . Gen.int $ Range.linear 1 50
bitmap <- fromValues values
let expected = Set.toAscList $ Set.fromList values
forward <- placed Iterator.init bitmap
readAll Iterator.read chunk forward `returns` expected
backward <- placed Iterator.initLast bitmap
readAll Iterator.readBackward chunk backward `returns` reverse expected
readAll :: (Storable value, Integral count) => (iterator -> Ptr value -> count -> IO count) -> Int -> iterator -> IO [value]
readAll readNext chunk iterator =
allocaArray chunk $ \buffer -> do
let loop = do
count <- fromIntegral <$> readNext iterator buffer (fromIntegral chunk)
found <- peekArray count buffer
if count < chunk then pure found else (found <>) <$> loop
loop
ranges :: Property
ranges =
Property.generated $ do
values <- forAll genValues
bitmap <- fromValues values
let expected = modelRanges . Set.toAscList $ Set.fromList values
forward <- placed Iterator.init bitmap
readRangesWith Iterator.readRanges forward `returns` expected
backward <- placed Iterator.initLast bitmap
readRangesWith Iterator.readPrevRanges backward `returns` reverse expected
where
readRangesWith readNext iterator =
allocaArray 3 $ \buffer -> do
let loop = do
count <- fromIntegral <$> readNext iterator buffer 3
found <- map (\range -> (range.min, range.max)) <$> peekArray count buffer
if count < 3 then pure found else (found <>) <$> loop
loop
modelRanges :: (Eq a, Num a) => [a] -> [(a, a)]
modelRanges = foldr extend []
where
extend value ((low, high) : rest)
| value + 1 == low = (value, high) : rest
extend value grouped = (value, value) : grouped
moves :: Property
moves =
Property.generated $ do
values <- forAll genValues
probes <- forAll . Gen.list (Range.linear 0 50) $ Gen.word32 Range.constantBounded
bitmap <- fromValues values
iterator <- Iterator <$> Property.alignedBytes Iterator.size Iterator.alignment
let
model = Set.fromList values
probe value = do
Iterator.init bitmap iterator
_ <- Iterator.moveEqualOrLarger iterator value
current iterator
forM probes probe `returns` map (`Set.lookupGE` model) probes
skips :: Property
skips =
Property.generated $ do
values <- forAll genValues
count <- forAll . Gen.int $ Range.linear 0 5000
bitmap <- fromValues values
let
model = Set.fromList values
total = Set.size model
skippable = fromIntegral $ min count total
forward <- placed Iterator.init bitmap
Iterator.skip forward (fromIntegral count) `returns` skippable
current forward `returns` if count < total then Just (Set.elemAt count model) else Nothing
backward <- placed Iterator.initLast bitmap
Iterator.skipBackward backward (fromIntegral count) `returns` skippable
current backward `returns` if count < total then Just (Set.elemAt (total - 1 - count) model) else Nothing
heap :: Property
heap =
Property.example $ do
bitmap <- fromValues [3, 70000, 9]
original <- created $ Iterator.create bitmap
Iterator.advance original `returns` True
copied <- created $ Iterator.copy original
Iterator.advance original `returns` True
Iterator.currentValue original `returns` 70000
walk Iterator.advance copied `returns` [9, 70000]
rangeStorable :: Property
rangeStorable =
Property.example $ do
let range = Range 5 99
ptr <- Property.array 1
evalIO $ poke ptr range
peek ptr `returns` range
scoped :: Property
scoped =
Property.generated $ do
values <- forAll genValues
bitmap <- fromValues values
let expected = Set.toAscList $ Set.fromList values
Iterator.with bitmap (walk Iterator.advance) `returns` expected
Iterator.withLast bitmap (walk Iterator.previous) `returns` reverse expected