kiroku-store-0.2.0.0: test/Test/ReadStream.hs
{- | Tests for "Kiroku.Store.Read".'readStreamForwardStream' — the
Streamly-shaped sibling of 'readStreamForward'. The streaming wrapper
must yield the same events in the same order as the @Vector@-returning
form, page transparently across the configured @pageSize@, terminate on
empty / nonexistent streams, and honor a non-zero starting cursor.
-}
module Test.ReadStream (spec) where
import Control.Lens ((^.))
import Data.Aeson qualified as Aeson
import Data.Generics.Labels ()
import Data.List.NonEmpty qualified as NE
import Data.Text qualified as T
import Data.Vector qualified as V
import Kiroku.Store
import Streamly.Data.Fold qualified as Fold
import Streamly.Data.Stream qualified as Stream
import Test.Helpers (makeEvent, withTestStore)
import Test.Hspec
spec :: Spec
spec = around withTestStore $
describe "readStreamForwardStream" $ do
it "matches readStreamForward on a single-page stream" $ \store -> do
let name = StreamName "rsfs-single-page"
events = [makeEvent (T.pack ("E" <> show i)) (Aeson.object []) | i <- [1 .. 5 :: Int]]
Right _ <- runStoreIO store $ appendToStream name NoStream events
streamed <- runStoreIO store $ Stream.toList (readStreamForwardStream name (StreamVersion 0) 256)
vectored <- runStoreIO store $ readStreamForward name (StreamVersion 0) 256
case (streamed, vectored) of
(Right xs, Right v) -> xs `shouldBe` V.toList v
_ -> expectationFailure ("Unexpected error: " <> show streamed <> " / " <> show vectored)
it "folds 1000 events end-to-end with Fold.length across multiple pages" $ \store -> do
let name = StreamName "rsfs-multi-page"
total = 1000 :: Int
events = [makeEvent (T.pack ("E" <> show i)) (Aeson.object []) | i <- [1 .. total]]
Right _ <- runStoreIO store $ appendToStream name NoStream events
countResult <-
runStoreIO store $
Stream.fold Fold.length (readStreamForwardStream name (StreamVersion 0) 256)
countResult `shouldBe` Right total
allEvents <-
runStoreIO store $
Stream.toList (readStreamForwardStream name (StreamVersion 0) 256)
case allEvents of
Right xs -> case NE.nonEmpty xs of
Just ne -> do
let h = NE.head ne
l = NE.last ne
(h ^. #streamVersion) `shouldBe` StreamVersion 1
(h ^. #eventType) `shouldBe` EventType (T.pack "E1")
(l ^. #streamVersion) `shouldBe` StreamVersion (fromIntegral total)
(l ^. #eventType) `shouldBe` EventType (T.pack ("E" <> show total))
Nothing -> expectationFailure "Expected non-empty events list"
Left err -> expectationFailure ("Unexpected error: " <> show err)
it "preserves order and avoids duplicates / gaps when paging at pageSize 2" $ \store -> do
let name = StreamName "rsfs-page-boundary"
events = [makeEvent (T.pack ("E" <> show i)) (Aeson.object []) | i <- [1 .. 5 :: Int]]
Right _ <- runStoreIO store $ appendToStream name NoStream events
versions <-
runStoreIO store $
Stream.toList $
fmap (^. #streamVersion) (readStreamForwardStream name (StreamVersion 0) 2)
versions
`shouldBe` Right
[ StreamVersion 1
, StreamVersion 2
, StreamVersion 3
, StreamVersion 4
, StreamVersion 5
]
it "terminates immediately on a nonexistent stream" $ \store -> do
let name = StreamName "rsfs-never-created"
result <-
runStoreIO store $
Stream.toList (readStreamForwardStream name (StreamVersion 0) 256)
result `shouldBe` Right []
it "honors a non-zero starting cursor with exclusivity" $ \store -> do
let name = StreamName "rsfs-non-zero-cursor"
events = [makeEvent (T.pack ("E" <> show i)) (Aeson.object []) | i <- [1 .. 5 :: Int]]
Right _ <- runStoreIO store $ appendToStream name NoStream events
tailEvents <-
runStoreIO store $
Stream.toList (readStreamForwardStream name (StreamVersion 2) 256)
case tailEvents of
Right xs -> do
length xs `shouldBe` 3
map (^. #streamVersion) xs
`shouldBe` [StreamVersion 3, StreamVersion 4, StreamVersion 5]
Left err -> expectationFailure ("Unexpected error: " <> show err)