wireform-proto-0.2.0.0: test/Test/Proto/Derive.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
{- | Round-trip tests for 'Proto.TH.Derive': encode each fixture to bytes,
decode the bytes back, and compare. Exercises bare scalars, the
proto3 default-skip rule, ZigZag and fixed-width 'wireOverride',
an optional nested submessage, and the IDL-bridge entry point
'deriveProtoFromTranslated' for repeated, map, oneof, and enum
shapes.
-}
module Test.Proto.Derive (tests) where
import Data.ByteString qualified as BS
import Data.Map.Strict qualified as Map
import Data.Text (Text)
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.Instances ()
import Test.Proto.Derive.RegressionInstances (
RegInventory (..),
RegItem (..),
defaultRegInventory,
defaultRegItem,
)
import Test.Proto.Derive.RegressionTypes (
BridgeRegInventory (..),
BridgeRegItem (..),
)
import Test.Proto.Derive.RichInstances ()
import Test.Proto.Derive.RichTypes (
Avatar (..),
Color (..),
Inventory (..),
Item (..),
LooseInventory (..),
Painting (..),
Profile (..),
Tagged (..),
)
import Test.Proto.Derive.TranslatedInstances ()
import Test.Proto.Derive.TranslatedTypes (AddressT (..), UserT (..))
import Test.Proto.Derive.Types (Address (..), User (..))
import Test.Syd
tests :: Spec
tests =
describe
"Proto.TH.Derive"
$ sequence_
[ it "default User round-trips" $ do
let u = defaultUser
let bs = PE.encodeMessage u
bs `shouldBe` BS.empty -- proto3 default: every field skipped
PD.decodeMessage bs `shouldBe` Right u
, it "scalar fields round-trip" $ do
let u =
defaultUser
{ userId = 42
, userName = T.pack "alice"
, userActive = True
, userScore = 3.14
, userTagBits = 0xDEADBEEFCAFEBABE
, userBlob = BS.pack [0, 1, 2, 3, 254, 255]
}
roundTrip u
, it "ZigZag override on negative sint32" $ do
let u = defaultUser {userOffset = -123456}
roundTrip u
, it "fixed32 override on uint32" $ do
let u = defaultUser {userPort = 0xCAFEBABE}
roundTrip u
-- The fixed32 wire payload always occupies 4 bytes plus 1 tag byte;
-- assert the encoded length to confirm we picked the right encoding.
BS.length (PE.encodeMessage u) `shouldBe` 5
, it "Maybe Address present round-trips" $ do
let addr =
Address
{ addrStreet = T.pack "1 Wireform Way"
, addrCity = T.pack "Berlin"
, addrZip = 10119
}
let u = defaultUser {userId = 7, userAddr = Just addr}
roundTrip u
, it "Maybe Address absent skips field" $ do
let u = defaultUser {userId = 7}
let bs = PE.encodeMessage u
-- Tag for field 9 is (9 << 3) | 2 = 74; assert it's NOT in the stream.
(74 `BS.notElem` bs) `shouldBe` True
PD.decodeMessage bs `shouldBe` Right u
, it "deriveProtoFromTranslated: round-trip preserves UserT" $ do
let u =
(defaultUserT :: UserT)
{ tuserId = 99
, tuserName = T.pack "carol"
, tuserActive = True
, tuserScore = 2.71
, tuserTagBits = 0xFEEDFACECAFEBEEF
, tuserBlob = BS.pack [0xDE, 0xAD, 0xBE, 0xEF]
, tuserOffset = -98765
, tuserPort = 0xCAFEBABE
, tuserAddr =
Just
AddressT
{ taddrStreet = T.pack "1 Wireform Way"
, taddrCity = T.pack "Berlin"
, taddrZip = 10119
}
}
PD.decodeMessage (PE.encodeMessage u) `shouldBe` Right u
, it "deriveProtoFromTranslated: byte-identical to deriveProto" $ do
let u =
defaultUser
{ userId = 99
, userName = T.pack "carol"
, userActive = True
, userScore = 2.71
, userTagBits = 0xFEEDFACECAFEBEEF
, userBlob = BS.pack [0xDE, 0xAD, 0xBE, 0xEF]
, userOffset = -98765
, userPort = 0xCAFEBABE
, userAddr =
Just
Address
{ addrStreet = T.pack "1 Wireform Way"
, addrCity = T.pack "Berlin"
, addrZip = 10119
}
}
uT =
(defaultUserT :: UserT)
{ tuserId = userId u
, tuserName = userName u
, tuserActive = userActive u
, tuserScore = userScore u
, tuserTagBits = userTagBits u
, tuserBlob = userBlob u
, tuserOffset = userOffset u
, tuserPort = userPort u
, tuserAddr =
fmap
( \a ->
AddressT
{ taddrStreet = addrStreet a
, taddrCity = addrCity a
, taddrZip = addrZip a
}
)
(userAddr u)
}
PE.encodeMessage uT `shouldBe` PE.encodeMessage u
, -- ---------------------------------------------------------------
-- Enum
-- ---------------------------------------------------------------
it "Painting (enum field): default round-trips" $ do
let p = Painting {pTitle = T.empty, pColor = ColRed}
let bs = PE.encodeMessage p
bs `shouldBe` BS.empty
PD.decodeMessage bs `shouldBe` Right p
, it "Painting (enum field): non-default round-trips" $ do
let p = Painting {pTitle = T.pack "Composition VIII", pColor = ColBlue}
PD.decodeMessage (PE.encodeMessage p) `shouldBe` Right p
, it "Painting (enum field): zero-valued enum is skipped" $ do
let p = Painting {pTitle = T.pack "X", pColor = ColRed}
let bs = PE.encodeMessage p
-- field 2 (color) has tag byte (2 << 3) | 0 = 16; assert it's
-- absent because ColRed = 0 is the proto default.
(16 `BS.notElem` bs) `shouldBe` True
, -- ---------------------------------------------------------------
-- Repeated
-- ---------------------------------------------------------------
it "Inventory (Vector-repeated submessages): empty round-trips" $ do
let inv = Inventory {invName = T.empty, invItems = V.empty}
PD.decodeMessage (PE.encodeMessage inv) `shouldBe` Right inv
, it "Inventory (Vector-repeated submessages): three elements" $ do
let items =
V.fromList
[ Item {iName = T.pack "alpha", iCount = 1}
, Item {iName = T.pack "beta", iCount = 2}
, Item {iName = T.pack "gamma", iCount = 3}
]
inv = Inventory {invName = T.pack "warehouse", invItems = items}
PD.decodeMessage (PE.encodeMessage inv) `shouldBe` Right inv
, it "LooseInventory (list-repeated strings): preserves order" $ do
let li =
LooseInventory
{ liId = 99
, liTags = [T.pack "a", T.pack "b", T.pack "c"]
}
PD.decodeMessage (PE.encodeMessage li) `shouldBe` Right li
, -- ---------------------------------------------------------------
-- Map
-- ---------------------------------------------------------------
it "Tagged (map<string, string>): empty map round-trips" $ do
let t = Tagged {tagName = T.empty, tagAttrs = Map.empty}
let bs = PE.encodeMessage t
bs `shouldBe` BS.empty
PD.decodeMessage bs `shouldBe` Right t
, it "Tagged (map<string, string>): three entries round-trip" $ do
let attrs =
Map.fromList
[ (T.pack "color", T.pack "red")
, (T.pack "size", T.pack "large")
, (T.pack "shape", T.pack "round")
]
t = Tagged {tagName = T.pack "demo", tagAttrs = attrs}
PD.decodeMessage (PE.encodeMessage t) `shouldBe` Right t
, -- ---------------------------------------------------------------
-- Oneof
-- ---------------------------------------------------------------
it "Profile (oneof): unset oneof round-trips" $ do
let p = Profile {profName = T.empty, profAvatar = Nothing}
let bs = PE.encodeMessage p
bs `shouldBe` BS.empty
PD.decodeMessage bs `shouldBe` Right p
, it "Profile (oneof): AvatarUrl variant round-trips" $ do
let p =
Profile
{ profName = T.pack "ada"
, profAvatar = Just (AvatarUrl (T.pack "https://example.test/x.png"))
}
PD.decodeMessage (PE.encodeMessage p) `shouldBe` Right p
, it "Profile (oneof): AvatarBlob variant round-trips" $ do
let p =
Profile
{ profName = T.pack "grace"
, profAvatar = Just (AvatarBlob (BS.pack [0xDE, 0xAD, 0xBE, 0xEF]))
}
PD.decodeMessage (PE.encodeMessage p) `shouldBe` Right p
, it "Profile (oneof): AvatarSeed variant round-trips" $ do
let p =
Profile
{ profName = T.pack "joan"
, profAvatar = Just (AvatarSeed 42)
}
PD.decodeMessage (PE.encodeMessage p) `shouldBe` Right p
, it "Profile (oneof): later variant wins on the wire" $ do
-- Concatenate two oneof field encodings; per proto3 spec the
-- last-wins, so encoding a Seed-only profile and decoding a
-- Url+Seed concatenation should yield the Seed variant.
let pUrl =
Profile
{ profName = T.empty
, profAvatar = Just (AvatarUrl (T.pack "old"))
}
pSeed =
Profile
{ profName = T.empty
, profAvatar = Just (AvatarSeed 7)
}
combined = PE.encodeMessage pUrl `BS.append` PE.encodeMessage pSeed
PD.decodeMessage combined `shouldBe` Right pSeed
, -- ---------------------------------------------------------------
-- Byte-equivalence regression: deriveProtoFromTranslated vs. loadProto
-- ---------------------------------------------------------------
--
-- 'Proto.TH.loadProto' is the long-standing proto code generator
-- and the implementation of record. The new IDL bridge
-- ('deriveProtoFromTranslated') must produce wire bytes that
-- match it for the same logical message; otherwise downstream
-- consumers that switch from one to the other would observe
-- silent corruption.
it "regression: empty RegInventory matches BridgeRegInventory bytes" $ do
let pBridge = BridgeRegInventory {briName = T.empty, briItems = V.empty}
pProto = defaultRegInventory
PE.encodeMessage pBridge `shouldBe` PE.encodeMessage pProto
, it "regression: name-only RegInventory matches" $ do
let pBridge =
BridgeRegInventory
{ briName = T.pack "warehouse-7"
, briItems = V.empty
}
pProto = defaultRegInventory {regInventoryReginvName = T.pack "warehouse-7"}
PE.encodeMessage pBridge `shouldBe` PE.encodeMessage pProto
, it "regression: repeated submessages produce identical bytes" $ do
let bridgeItems =
V.fromList
[ BridgeRegItem (T.pack "alpha") 1
, BridgeRegItem (T.pack "beta") 2
, BridgeRegItem (T.pack "gamma") 3
]
protoItems =
V.fromList
[ defaultRegItem {regItemRegiName = T.pack "alpha", regItemRegiCount = 1}
, defaultRegItem {regItemRegiName = T.pack "beta", regItemRegiCount = 2}
, defaultRegItem {regItemRegiName = T.pack "gamma", regItemRegiCount = 3}
]
pBridge =
BridgeRegInventory
{ briName = T.pack "depot"
, briItems = bridgeItems
}
pProto =
defaultRegInventory
{ regInventoryReginvName = T.pack "depot"
, regInventoryReginvItems = protoItems
}
PE.encodeMessage pBridge `shouldBe` PE.encodeMessage pProto
, it "regression: single RegItem matches BridgeRegItem bytes" $ do
let pBridge = BridgeRegItem (T.pack "widget") 99
pProto = defaultRegItem {regItemRegiName = T.pack "widget", regItemRegiCount = 99}
PE.encodeMessage pBridge `shouldBe` PE.encodeMessage pProto
]
defaultUserT :: UserT
defaultUserT =
UserT
{ tuserId = 0
, tuserName = T.empty
, tuserActive = False
, tuserScore = 0
, tuserTagBits = 0
, tuserBlob = BS.empty
, tuserOffset = 0
, tuserPort = 0
, tuserAddr = Nothing
}
defaultUser :: User
defaultUser =
User
{ userId = 0
, userName = T.empty
, userActive = False
, userScore = 0
, userTagBits = 0
, userBlob = BS.empty
, userOffset = 0
, userPort = 0
, userAddr = Nothing
}
roundTrip :: User -> IO ()
roundTrip u = PD.decodeMessage (PE.encodeMessage u) `shouldBe` Right u