wireform-proto-0.2.0.0: test/Test/Proto/Derive/TopEnum.hs
{- | Tests for the top-level enum + packed scalar 'loadProto'
regression.
-}
module Test.Proto.Derive.TopEnum (tests) where
import Data.ByteString qualified as BS
import Data.Text qualified as T
import Data.Vector qualified as V
import Proto.Decode qualified as PD
import Proto.Encode qualified as PE
import Test.Proto.Derive.TopEnumInstances (
Account (..),
PackedBag (..),
Status (..),
defaultAccount,
defaultPackedBag,
)
import Test.Syd
tests :: Spec
tests =
describe
"Proto.TH top-level enum + packed scalar"
$ sequence_
[ describe
"top-level enum field encodes as varint, not submessage"
$ sequence_
[ it "default Status (UNSPECIFIED = 0) is skipped" $ do
let a = defaultAccount
PE.encodeMessage a `shouldBe` BS.empty
, it "Status = STATUS_ACTIVE encodes as varint 1, not as a submessage" $ do
let a = defaultAccount {accountAcctStatus = Status'StatusActive}
-- field 2 (varint) tag = (2<<3)|0 = 0x10; payload = 1.
-- If the bridge had wrongly chosen PFSubmessage we'd
-- see 0x12 ((2<<3)|2) followed by a length prefix here.
PE.encodeMessage a `shouldBe` BS.pack [0x10, 0x01]
, it "Status = STATUS_BANNED round-trips" $ do
let a =
defaultAccount
{ accountAcctName = T.pack "anon"
, accountAcctStatus = Status'StatusBanned
}
PD.decodeMessage (PE.encodeMessage a) `shouldBe` Right a
, it "all four Status values round-trip" $ do
let go st = do
let a = defaultAccount {accountAcctStatus = st}
PD.decodeMessage (PE.encodeMessage a) `shouldBe` Right a
mapM_
go
[ Status'StatusUnspecified
, Status'StatusActive
, Status'StatusRetired
, Status'StatusBanned
]
, it "enum decoder truncates oversized varint to int32" $ do
-- Proto3 wire spec: enum values are int32 on the wire
-- even though varints can carry larger values. A sender
-- that writes a 10-byte varint of @kInt64Max@ for an
-- enum field is expected to be parsed as the int32
-- truncation (low 32 bits, sign-extended). For
-- 0xFFFFFFFFFFFFFFFF (the proto3-canonical encoding of
-- -1 as a 10-byte varint), the int32 truncation is -1,
-- which our generated 'Status' has no constructor for —
-- so it falls through to the catch-all (the first
-- declared, Status'StatusUnspecified). The point of this test
-- is to confirm we don't crash and the message
-- round-trips.
let bs =
BS.pack
[ 0x10 -- field 2 (acct_status) varint
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0xFF
, 0x01 -- 10-byte varint of -1
]
case PD.decodeMessage bs :: Either PD.DecodeError Account of
Right _ -> pure () -- decode succeeded; that's what matters
Left e -> expectationFailure ("expected decode success, got " <> show e)
]
, describe
"packed scalar (proto3 spec default for repeated int32)"
$ sequence_
[ it "empty bag encodes to 0 bytes" $ do
PE.encodeMessage defaultPackedBag `shouldBe` BS.empty
, it "Vector [1,2,3] is one length-delimited block" $ do
let p = defaultPackedBag {packedBagBagNums = V.fromList [1, 2, 3]}
PE.encodeMessage p `shouldBe` BS.pack [0x0A, 0x03, 0x01, 0x02, 0x03]
PD.decodeMessage (PE.encodeMessage p) `shouldBe` Right p
, it "decoder still accepts unpacked encoding" $ do
-- Hand-craft an unpacked stream, assert it produces
-- the same Vector.
let unpacked =
BS.pack
[0x08, 0x01, 0x08, 0x02, 0x08, 0x03]
expected = defaultPackedBag {packedBagBagNums = V.fromList [1, 2, 3]}
PD.decodeMessage unpacked `shouldBe` Right expected
, it "encoded length stays small as element count grows" $ do
-- Strongest packed-vs-unpacked check that doesn't depend
-- on counting tag bytes (which can collide with payload
-- values for small ints). Unpacked would emit
-- 1 tag byte + 1 payload byte = 2 bytes per element.
-- Packed emits
-- 1 tag byte + 1 length byte + 1 payload byte each.
-- So 100 single-byte values should fit in well under
-- the 200 bytes an unpacked stream would take.
let p = defaultPackedBag {packedBagBagNums = V.fromList [1 .. 100]}
bs = PE.encodeMessage p
( if (BS.length bs <= 110)
then pure ()
else
expectationFailure
( "expected packed encoding (≤ 110 bytes), got "
<> show (BS.length bs)
)
)
]
]