store-0.1.0.0: test/Data/Store/StreamingSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Store.StreamingSpec where
import Control.Monad.Trans.Resource
import Control.Exception (try)
import qualified Data.ByteString as BS
import Data.Conduit ((=$=), ($$))
import qualified Data.Conduit.List as C
import Data.List (unfoldr)
import Data.Monoid
import Data.Store (PeekException (..))
import Data.Store.Streaming
import qualified System.IO.ByteBuffer as BB
import Test.Hspec
import Test.Hspec.SmallCheck
import Test.SmallCheck
spec :: Spec
spec = do
describe "conduitEncode and conduitDecode" $ do
it "Roundtrips ([Int])." $ property roundtrip
it "Roundtrips ([Int]), with chunked transfer." $ property roundtripChunked
it "Throws an Exception on incomplete messages." conduitIncomplete
it "Throws an Exception on excess input." $ property conduitExcess
describe "peekMessage" $ do
it "demands more input when needed." $ property (askMore 8)
it "demands more input on incomplete SizeTag." $ property (askMore 1)
it "successfully decodes valid input." $ property peek
describe "decodeMessage" $
it "Throws an Exception on incomplete messages." decodeIncomplete
roundtrip :: [Int] -> Property IO
roundtrip xs = monadic $ do
xs' <- runResourceT $ C.sourceList xs
=$= C.map Message
=$= conduitEncode
=$= conduitDecode Nothing
=$= C.map fromMessage
$$ C.consume
return $ xs' == xs
roundtripChunked :: [Int] -> Property IO
roundtripChunked input = monadic $ do
let (xs, chunkLengths) = splitAt (length input `div` 2) input
bs <- C.sourceList xs
=$= C.map Message
=$= conduitEncode
$$ C.fold (<>) mempty
let chunks = unfoldr takeChunk (bs, chunkLengths)
xs' <- runResourceT $ C.sourceList chunks
=$= conduitDecode (Just 10)
=$= C.map fromMessage
$$ C.consume
return $ xs' == xs
where
takeChunk (x, _) | BS.null x = Nothing
takeChunk (x, []) = Just (x, (BS.empty, []))
takeChunk (x, l:ls) =
let (chunk, rest) = BS.splitAt l x
in Just (chunk, (rest, ls))
conduitIncomplete :: Expectation
conduitIncomplete =
(runResourceT (C.sourceList [incompleteInput]
=$= conduitDecode (Just 10)
$$ C.consume)
:: IO [Message Integer]) `shouldThrow` peekException
conduitExcess :: [Int] -> Property IO
conduitExcess xs = monadic $ do
bs <- C.sourceList xs
=$= C.map Message
=$= conduitEncode
$$ C.fold (<>) mempty
res <- try (runResourceT (C.sourceList [bs `BS.append` "excess bytes"]
=$= conduitDecode (Just 10)
$$ C.consume) :: IO [Message Int])
case res of
Right _ -> return False
Left (PeekException _ _) -> return True
-- splits an encoded message after n bytes. Feeds the first part to
-- peekResult, expecting it to require more input. Then, feeds the
-- second part and checks if the decoded result is the original
-- message.
askMore :: Int -> Integer -> Property IO
askMore n x = monadic $ BB.with (Just 10) $ \ bb -> do
let bs = encodeMessage (Message x)
(start, end) = BS.splitAt n $ bs
BB.copyByteString bb start
peekResult <- peekMessage bb :: IO (PeekMessage IO Integer)
case peekResult of
NeedMoreInput cont ->
cont end >>= \case
Done (Message x') -> return $ x' == x
_ -> return False
_ -> return False
peek :: Integer -> Property IO
peek x = monadic $ BB.with (Just 10) $ \ bb -> do
let bs = encodeMessage (Message x)
BB.copyByteString bb bs
peekResult <- peekMessage bb :: IO (PeekMessage IO Integer)
case peekResult of
NeedMoreInput _ -> return False
Done (Message x') -> return $ x' == x
decodeIncomplete :: IO ()
decodeIncomplete = BB.with (Just 0) $ \ bb -> do
BB.copyByteString bb (BS.take 1 incompleteInput)
(decodeMessage bb (return Nothing) :: IO (Maybe (Message Integer)))
`shouldThrow` peekException
incompleteInput :: BS.ByteString
incompleteInput =
let bs = encodeMessage (Message (42 :: Integer))
in BS.take (BS.length bs - 1) bs
peekException :: Selector PeekException
peekException = const True