wireform-proto-0.2.0.0: src/Proto/Conformance.hs
{-# LANGUAGE BangPatterns #-}
{- | Conformance test harness for the official protobuf conformance suite.
The conformance runner sends 'ConformanceRequest' messages via stdin
(length-prefixed) and expects 'ConformanceResponse' messages back.
To run:
@
conformance-test-runner --enforce_recommended ./wireform-conformance
@
-}
module Proto.Conformance (
-- * Conformance types
ConformanceRequest (..),
defaultConformanceRequest,
ConformanceResponse (..),
defaultConformanceResponse,
WireFormat (..),
-- * Conformance runner
conformanceMain,
handleConformanceRequest,
) where
import Control.DeepSeq (NFData)
import Data.Bits (shiftR, (.&.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Int (Int32)
import Data.Text (Text)
import Data.Word (Word32)
import GHC.Generics (Generic)
import Proto.Decode
import Proto.Encode
import Proto.Internal.Wire (Tag (..))
import System.IO (BufferMode (..), hFlush, hSetBinaryMode, hSetBuffering, isEOF, stdin, stdout)
data WireFormat = Protobuf | JSON | Jspb | TextFormat
deriving stock (Show, Eq, Ord, Enum, Bounded, Generic)
deriving anyclass (NFData)
data ConformanceRequest = ConformanceRequest
{ crPayload :: !ByteString
, crRequestedOutputFormat :: !Int32
, crMessageType :: !Text
}
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData)
defaultConformanceRequest :: ConformanceRequest
defaultConformanceRequest = ConformanceRequest "" 0 ""
instance MessageEncode ConformanceRequest where
buildMessage cr =
(if BS.null (crPayload cr) then mempty else encodeFieldBytes 1 (crPayload cr))
<> ( if crRequestedOutputFormat cr == 0
then mempty
else encodeFieldVarint 3 (fromIntegral (crRequestedOutputFormat cr))
)
<> (if crMessageType cr == "" then mempty else encodeFieldString 4 (crMessageType cr))
instance MessageDecode ConformanceRequest where
messageDecoder = loop defaultConformanceRequest
where
loop !cr = do
mt <- getTagOrU
case mt of
UNothing -> pure cr
UJust (Tag 1 _) -> do v <- decodeFieldBytes; loop cr {crPayload = v}
UJust (Tag 2 _) -> do _jsonPayload <- decodeFieldString; loop cr {crPayload = BS.empty}
UJust (Tag 3 _) -> do v <- getVarint; loop cr {crRequestedOutputFormat = fromIntegral v}
UJust (Tag 4 _) -> do v <- decodeFieldString; loop cr {crMessageType = v}
UJust (Tag _ wt) -> skipField wt >> loop cr
newtype ConformanceResponse = ConformanceResponse
{ crsResult :: ConformanceResult
}
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData)
data ConformanceResult
= ParseError !Text
| SerializeError !Text
| RuntimeError !Text
| ProtobufPayload !ByteString
| JsonPayload !Text
| Skipped !Text
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData)
defaultConformanceResponse :: ConformanceResponse
defaultConformanceResponse = ConformanceResponse (Skipped "")
instance MessageEncode ConformanceResponse where
buildMessage (ConformanceResponse r) = case r of
ParseError t -> encodeFieldString 1 t
RuntimeError t -> encodeFieldString 2 t
ProtobufPayload b -> encodeFieldBytes 3 b
JsonPayload t -> encodeFieldString 4 t
Skipped t -> encodeFieldString 5 t
SerializeError t -> encodeFieldString 6 t
instance MessageDecode ConformanceResponse where
messageDecoder = loop defaultConformanceResponse
where
loop !cr = do
mt <- getTagOrU
case mt of
UNothing -> pure cr
UJust (Tag 1 _) -> ConformanceResponse . ParseError <$> decodeFieldString
UJust (Tag 2 _) -> ConformanceResponse . RuntimeError <$> decodeFieldString
UJust (Tag 3 _) -> ConformanceResponse . ProtobufPayload <$> decodeFieldBytes
UJust (Tag 4 _) -> ConformanceResponse . JsonPayload <$> decodeFieldString
UJust (Tag 5 _) -> ConformanceResponse . Skipped <$> decodeFieldString
UJust (Tag 6 _) -> ConformanceResponse . SerializeError <$> decodeFieldString
UJust (Tag _ wt) -> skipField wt >> loop cr
{- | Main loop for conformance testing.
Reads length-delimited ConformanceRequest from stdin,
processes them, and writes length-delimited ConformanceResponse to stdout.
-}
conformanceMain :: (ConformanceRequest -> IO ConformanceResponse) -> IO ()
conformanceMain handler = do
hSetBinaryMode stdin True
hSetBinaryMode stdout True
hSetBuffering stdout NoBuffering
go
where
go = do
eof <- isEOF
if eof
then pure ()
else do
lenBytes <- BS.hGet stdin 4
if BS.length lenBytes < 4
then pure ()
else do
let len = fromIntegral (readLE32 lenBytes)
payload <- BS.hGet stdin len
case decodeMessage payload of
Left _err -> do
let resp = ConformanceResponse (ParseError "Failed to decode ConformanceRequest")
writeResponse resp
go
Right req -> do
resp <- handler req
writeResponse resp
go
writeResponse resp = do
let encoded = encodeMessage resp
lenBytes = encodeLE32 (fromIntegral (BS.length encoded))
BS.hPut stdout lenBytes
BS.hPut stdout encoded
hFlush stdout
readLE32 :: ByteString -> Word32
readLE32 bs =
let b0 = fromIntegral (BS.index bs 0) :: Word32
b1 = fromIntegral (BS.index bs 1) :: Word32
b2 = fromIntegral (BS.index bs 2) :: Word32
b3 = fromIntegral (BS.index bs 3) :: Word32
in b0 + b1 * 256 + b2 * 65536 + b3 * 16777216
encodeLE32 :: Word32 -> ByteString
encodeLE32 n =
BS.pack
[ fromIntegral (n .&. 0xFF)
, fromIntegral ((n `shiftR` 8) .&. 0xFF)
, fromIntegral ((n `shiftR` 16) .&. 0xFF)
, fromIntegral ((n `shiftR` 24) .&. 0xFF)
]
-- | Default handler that does protobuf round-trip.
handleConformanceRequest :: ConformanceRequest -> IO ConformanceResponse
handleConformanceRequest _req =
pure (ConformanceResponse (Skipped "Not yet implemented"))