packages feed

wireform-proto-0.1.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 qualified as PD
import Proto qualified as PE
import Test.Proto.Derive.TopEnumInstances (
  Account (..),
  PackedBag (..),
  Status (..),
  defaultAccount,
  defaultPackedBag,
 )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))


tests :: TestTree
tests =
  testGroup
    "Proto.TH top-level enum + packed scalar"
    [ testGroup
        "top-level enum field encodes as varint, not submessage"
        [ testCase "default Status (UNSPECIFIED = 0) is skipped" $ do
            let a = defaultAccount
            PE.encodeMessage a @?= BS.empty
        , testCase "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 @?= BS.pack [0x10, 0x01]
        , testCase "Status = STATUS_BANNED round-trips" $ do
            let a =
                  defaultAccount
                    { accountAcctName = T.pack "anon"
                    , accountAcctStatus = Status'StatusBanned
                    }
            PD.decodeMessage (PE.encodeMessage a) @?= Right a
        , testCase "all four Status values round-trip" $ do
            let go st = do
                  let a = defaultAccount {accountAcctStatus = st}
                  PD.decodeMessage (PE.encodeMessage a) @?= Right a
            mapM_
              go
              [ Status'StatusUnspecified
              , Status'StatusActive
              , Status'StatusRetired
              , Status'StatusBanned
              ]
        , testCase "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 -> assertFailure ("expected decode success, got " <> show e)
        ]
    , testGroup
        "packed scalar (proto3 spec default for repeated int32)"
        [ testCase "empty bag encodes to 0 bytes" $ do
            PE.encodeMessage defaultPackedBag @?= BS.empty
        , testCase "Vector [1,2,3] is one length-delimited block" $ do
            let p = defaultPackedBag {packedBagBagNums = V.fromList [1, 2, 3]}
            PE.encodeMessage p @?= BS.pack [0x0A, 0x03, 0x01, 0x02, 0x03]
            PD.decodeMessage (PE.encodeMessage p) @?= Right p
        , testCase "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 @?= Right expected
        , testCase "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
            assertBool
              ( "expected packed encoding (≤ 110 bytes), got "
                  <> show (BS.length bs)
              )
              (BS.length bs <= 110)
        ]
    ]