store-0.2.0.0: test/Data/Store/StreamingSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Store.StreamingSpec where
import Control.Exception (try)
import Control.Monad (void)
import Control.Monad.Trans.Resource
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import Data.Conduit ((=$=), ($$))
import qualified Data.Conduit.List as C
import Data.Int
import Data.List (unfoldr)
import Data.Monoid
import Data.Store.Core (Poke(..))
import Data.Store.Internal
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 17)
it "demands more input on incomplete message magic." $ property (askMore 1)
it "demands more input on incomplete SizeTag." $ property (askMore 9)
it "successfully decodes valid input." $ property canPeek
describe "decodeMessage" $ do
it "Throws an Exception on incomplete messages." decodeIncomplete
it "Throws an Exception on messages that are shorter than indicated." decodeTooShort
it "Throws an Exception on messages that are longer than indicated." decodeTooLong
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{} -> True
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
canPeek :: Integer -> Property IO
canPeek 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{} -> True
incompleteInput :: BS.ByteString
incompleteInput =
let bs = encodeMessage (Message (42 :: Integer))
in BS.take (BS.length bs - 1) bs
decodeTooLong :: IO ()
decodeTooLong = BB.with Nothing $ \bb -> do
BB.copyByteString bb (encodeMessageTooLong . Message $ (1 :: Int))
(decodeMessage bb (return Nothing) :: IO (Maybe (Message Int)))
`shouldThrow` \PeekException{} -> True
decodeTooShort :: IO ()
decodeTooShort = BB.with Nothing $ \bb -> do
BB.copyByteString bb (encodeMessageTooShort . Message $ (1 :: Int))
(decodeMessage bb (return Nothing) :: IO (Maybe (Message Int)))
`shouldThrow` (== PeekException 8 "Attempted to read too many bytes for Data.Store.Message.SizeTag. Needed 16, but only 8 remain.")
encodeMessageTooLong :: Store a => Message a -> BS.ByteString
encodeMessageTooLong (Message x) =
let l = 8 + getSize x
totalLength = 8 + l
in BS.unsafeCreate
totalLength
(\p -> void $ runPoke (poke l >> poke x >> poke (0::Int64)) p 0)
encodeMessageTooShort :: Store a => Message a -> BS.ByteString
encodeMessageTooShort (Message x) =
let l = 0
totalLength = 8 + l
in BS.unsafeCreate
totalLength
(\p -> void $ runPoke (poke l >> poke x) p 0)