wireform-proto-0.2.0.0: test-integration/Test/StreamCodec.hs
module Test.StreamCodec (streamCodecTests) where
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Text (Text)
import Data.Word (Word64)
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Proto.Decode (MessageDecode (..))
import Proto.Decode.Stream (
IDecode (..),
decodeMessageIncremental,
decodeMessageLazy,
decodeMessageStream,
)
import Proto.Encode (MessageEncode (..), MessageSize (..), encodeMessage, encodeMessageLazy, encodeMessageSized, encodeMessageStream, encodeMessageStreamSized)
import Proto.Internal.Wire (Tag (..), WireType (..))
import Proto.Internal.Wire.Decode (DecodeError (..), Decoder, getTagOr, getText, getVarint, skipField)
import Proto.Internal.Wire.Encode (fieldBoolSize, fieldTextSize, fieldVarintSize, putTag, putText, putVarint)
import Test.Syd
import Test.Syd.Hedgehog ()
import Wireform.Builder qualified as B
streamCodecTests :: Spec
streamCodecTests =
describe
"Streaming & Lazy Codecs"
$ sequence_
[ lazyEncodeTests
, lazyDecodeTests
, streamRoundtripTests
, incrementalDecodeTests
]
-- -----------------------------------------------------------------------
-- Lazy single-message encoding
-- -----------------------------------------------------------------------
lazyEncodeTests :: Spec
lazyEncodeTests =
describe
"Lazy encoding"
$ sequence_
[ it "encodeMessageLazy matches strict" $ property $ do
msg <- genSMsg
BL.toStrict (encodeMessageLazy msg) === encodeMessage msg
, it "encodeMessageLazy matches strict" $ property $ do
msg <- genSMsg
BL.toStrict (encodeMessageLazy msg) === encodeMessageSized msg
, it "encodeMessageLazy matches encodeMessageLazy" $ property $ do
msg <- genSMsg
encodeMessageLazy msg === encodeMessageLazy msg
]
-- -----------------------------------------------------------------------
-- Lazy single-message decoding
-- -----------------------------------------------------------------------
lazyDecodeTests :: Spec
lazyDecodeTests =
describe
"Lazy decoding"
$ sequence_
[ it "decodeMessageLazy roundtrip" $ property $ do
msg <- genSMsg
let lbs = encodeMessageLazy msg
decodeMessageLazy lbs === Right msg
, it "decodeMessageLazy empty" $ do
let lbs = BL.empty
decodeMessageLazy lbs `shouldBe` Right (SMsg 0 "" False)
, it "decodeMessageLazy multi-chunk" $ do
let strict = encodeMessage (SMsg 42 "hello" True)
(a, b) = BS.splitAt (BS.length strict `div` 2) strict
lbs = BL.fromChunks [a, b]
decodeMessageLazy lbs `shouldBe` Right (SMsg 42 "hello" True)
]
-- -----------------------------------------------------------------------
-- Stream framing roundtrip
-- -----------------------------------------------------------------------
streamRoundtripTests :: Spec
streamRoundtripTests =
describe
"Stream framing roundtrip"
$ sequence_
[ it "stream roundtrip (no size)" $ property $ do
msgs <- forAll $ Gen.list (Range.linear 0 20) genSMsg'
let encoded = encodeMessageStream msgs
decoded = decodeMessageStream encoded
fmap fromRight' decoded === msgs
, it "stream roundtrip (sized)" $ property $ do
msgs <- forAll $ Gen.list (Range.linear 0 20) genSMsg'
let encoded = encodeMessageStreamSized msgs
decoded = decodeMessageStream encoded
fmap fromRight' decoded === msgs
, it "sized stream matches non-sized stream" $ property $ do
msgs <- forAll $ Gen.list (Range.linear 0 20) genSMsg'
encodeMessageStreamSized msgs === encodeMessageStream msgs
, it "empty stream" $ do
let encoded = encodeMessageStream ([] :: [SMsg])
BL.null encoded `shouldBe` True
decodeMessageStream @SMsg encoded `shouldBe` []
, it "single-message stream" $ do
let msg = SMsg 99 "solo" True
encoded = encodeMessageStream [msg]
decoded = decodeMessageStream encoded
decoded `shouldBe` [Right msg]
, it "stream decode truncated length" $ do
let lbs = BL.pack [0x80]
case decodeMessageStream @SMsg lbs of
[Left UnexpectedEnd] -> pure ()
other -> expectationFailure ("Expected [Left UnexpectedEnd], got: " <> show other)
, it "stream decode truncated payload" $ do
let lbs = BL.pack [0x0A, 0x01]
case decodeMessageStream @SMsg lbs of
[Left UnexpectedEnd] -> pure ()
other -> expectationFailure ("Expected [Left UnexpectedEnd], got: " <> show other)
, it "stream framing matches manual framing" $ property $ do
msgs <- forAll $ Gen.list (Range.linear 1 10) genSMsg'
let autoFramed = encodeMessageStream msgs
manualFramed = BL.fromChunks $ do
msg <- msgs
let payload = encodeMessage msg
pure $ buildToBS (putVarint (fromIntegral (BS.length payload)) <> B.byteString payload)
autoFramed === BL.fromStrict (BL.toStrict manualFramed)
]
-- -----------------------------------------------------------------------
-- Incremental decoder
-- -----------------------------------------------------------------------
incrementalDecodeTests :: Spec
incrementalDecodeTests =
describe
"Incremental decoder"
$ sequence_
[ it "single message fed all at once" $ property $ do
msg <- genSMsg
let framed = frameMessage msg
case feed decodeMessageIncremental framed of
IDone decoded leftover -> do
decoded === msg
BS.null leftover === True
other -> do
annotate (show other)
failure
, it "single message fed byte-by-byte" $ property $ do
msg <- genSMsg
let framed = frameMessage msg
chunks = fmap BS.singleton (BS.unpack framed)
case feedAll decodeMessageIncremental chunks of
IDone decoded leftover -> do
decoded === msg
BS.null leftover === True
other -> do
annotate (show other)
failure
, it "preserves leftover bytes" $ property $ do
msg <- genSMsg
extra <- forAll $ Gen.bytes (Range.linear 1 50)
let framed = frameMessage msg <> extra
case feed decodeMessageIncremental framed of
IDone decoded leftover -> do
decoded === msg
leftover === extra
other -> do
annotate (show other)
failure
, it "two messages fed together" $ property $ do
msg1 <- genSMsg
msg2 <- genSMsg
let framed = frameMessage msg1 <> frameMessage msg2
case feed decodeMessageIncremental framed of
IDone decoded1 leftover1 -> do
decoded1 === msg1
case feed decodeMessageIncremental leftover1 of
IDone decoded2 leftover2 -> do
decoded2 === msg2
BS.null leftover2 === True
other -> do
annotate ("second: " <> show other)
failure
other -> do
annotate ("first: " <> show other)
failure
, it "split at arbitrary byte boundary" $ property $ do
msg <- genSMsg
let framed = frameMessage msg
splitPos <- forAll $ Gen.int (Range.linear 0 (BS.length framed))
let (chunk1, chunk2) = BS.splitAt splitPos framed
dec0 = feed decodeMessageIncremental chunk1
case dec0 of
IDone decoded _ -> decoded === msg
IPartial _ ->
case feed dec0 chunk2 of
IDone decoded leftover -> do
decoded === msg
BS.null leftover === True
other -> do
annotate (show other)
failure
other -> do
annotate (show other)
failure
, it "empty message" $ do
let framed = BS.singleton 0
case feed decodeMessageIncremental framed of
IDone decoded leftover -> do
decoded `shouldBe` SMsg 0 "" False
BS.null leftover `shouldBe` True
other -> expectationFailure (show other)
, it "Nothing at start yields IFail" $ do
case decodeMessageIncremental @SMsg of
IPartial k -> case k Nothing of
IFail UnexpectedEnd _ -> pure ()
other -> expectationFailure ("Expected IFail UnexpectedEnd, got: " <> show other)
other -> expectationFailure ("Expected IPartial, got: " <> show other)
, it "truncated varint" $ do
let chunk = BS.pack [0x80, 0x80]
case feed decodeMessageIncremental chunk of
IPartial k -> case k Nothing of
IFail UnexpectedEnd _ -> pure ()
other -> expectationFailure ("Expected IFail UnexpectedEnd, got: " <> show (other :: IDecode SMsg))
other -> expectationFailure ("Expected IPartial, got: " <> show (other :: IDecode SMsg))
, it "truncated payload" $ do
let chunk = BS.pack [0x0A, 0x01]
case feed decodeMessageIncremental chunk of
IPartial k -> case k Nothing of
IFail UnexpectedEnd _ -> pure ()
other -> expectationFailure ("Expected IFail, got: " <> show (other :: IDecode SMsg))
other -> expectationFailure ("Expected IPartial, got: " <> show (other :: IDecode SMsg))
]
-- -----------------------------------------------------------------------
-- Incremental encoder
-- -----------------------------------------------------------------------
-- -----------------------------------------------------------------------
-- Test message type
-- -----------------------------------------------------------------------
data SMsg = SMsg
{ smValue :: {-# UNPACK #-} !Word64
, smName :: !Text
, smActive :: !Bool
}
deriving stock (Show, Eq)
instance MessageEncode SMsg where
buildMessage msg =
(if smValue msg /= 0 then putTag 1 WireVarint <> putVarint (smValue msg) else mempty)
<> (if smName msg /= "" then putTag 2 WireLengthDelimited <> putText (smName msg) else mempty)
<> (if smActive msg then putTag 3 WireVarint <> putVarint 1 else mempty)
instance MessageSize SMsg where
messageSize msg =
(if smValue msg /= 0 then fieldVarintSize 1 (smValue msg) else 0)
+ (if smName msg /= "" then fieldTextSize 2 (smName msg) else 0)
+ (if smActive msg then fieldBoolSize 3 else 0)
instance MessageDecode SMsg where
messageDecoder = loop 0 "" False
where
loop :: Word64 -> Text -> Bool -> Decoder SMsg
loop !val !name !active = do
mt <- getTagOr
case mt of
Nothing -> pure (SMsg val name active)
Just (Tag fn wt) -> case fn of
1 -> getVarint >>= \v -> loop v name active
2 -> getText >>= \v -> loop val v active
3 -> getVarint >>= \v -> loop val name (v /= 0)
_ -> skipField wt >> loop val name active
genSMsg :: PropertyT IO SMsg
genSMsg = do
v <- forAll $ Gen.word64 (Range.linear 0 1000000)
t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum
b <- forAll Gen.bool
pure (SMsg v t b)
genSMsg' :: Gen SMsg
genSMsg' =
SMsg
<$> Gen.word64 (Range.linear 0 1000000)
<*> Gen.text (Range.linear 0 100) Gen.alphaNum
<*> Gen.bool
fromRight' :: Either DecodeError a -> a
fromRight' (Right a) = a
fromRight' (Left e) = error ("unexpected decode error: " <> show e)
buildToBS :: B.Builder -> BS.ByteString
buildToBS = BL.toStrict . B.toLazyByteString
frameMessage :: (MessageEncode a) => a -> BS.ByteString
frameMessage msg =
let payload = encodeMessage msg
in buildToBS (putVarint (fromIntegral (BS.length payload)) <> B.byteString payload)
feed :: IDecode a -> BS.ByteString -> IDecode a
feed (IPartial k) bs = k (Just bs)
feed done _ = done
feedAll :: IDecode a -> [BS.ByteString] -> IDecode a
feedAll dec [] = case dec of
IPartial k -> k Nothing
other -> other
feedAll dec (c : cs) = case dec of
IPartial k -> feedAll (k (Just c)) cs
other -> other