wireform-proto-0.2.0.0: test-conformance/Runner.hs
{-# LANGUAGE OverloadedStrings #-}
{- | The @wireform-conformance-runner@ binary.
The upstream protobuf conformance_test_runner pipes
length-prefixed 'ConformanceRequest' messages over our stdin
and reads length-prefixed 'ConformanceResponse' messages back
from our stdout. This module is the IO loop; the per-request
logic lives in "Test.Conformance.Handler".
Length prefix is little-endian uint32 (NOT a varint — that's a
protocol detail of the runner, not the wire format).
== Manual smoke test
@
echo \-n "" | wireform-conformance-runner # exits silently on EOF
@
Real use is via 'Test.Conformance.Driver', which builds the
upstream runner and pipes requests through this binary.
-}
module Main (main) where
import Control.Exception (SomeException, catch, evaluate)
import Data.Aeson qualified as Aeson
import Data.Aeson.Types qualified as AesonT
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.ByteString qualified as BS
import Data.Proxy (Proxy (..))
import Data.Text qualified as T
import Data.Word (Word32)
import Proto.Decode qualified as PD
import Proto.Encode qualified as PE
import Proto.Internal.JSON.WellKnown qualified as WK
import Proto.Registry (AnyCodec (..), TypeRegistry, registerCodec)
import System.IO (
BufferMode (..),
Handle,
hFlush,
hPutStrLn,
hSetBinaryMode,
hSetBuffering,
isEOF,
stderr,
stdin,
stdout,
)
import Test.Conformance.Handler (handleRequest)
import Test.Conformance.Schema (
ConformanceResponse,
ConformanceResponse'Result (..),
TestAllTypesProto2,
TestAllTypesProto3,
conformanceResponseResult,
defaultConformanceResponse,
registerExt_protobuf_test_messages_proto2_extension_int32,
)
{- | The type registry used by the conformance runner: standard
WKT codecs plus the conformance suite's reference messages.
-}
conformanceRegistry :: TypeRegistry
conformanceRegistry =
registerCodec
"protobuf_test_messages.proto3.TestAllTypesProto3"
testAllTypesProto3AnyCodec
. registerCodec
"protobuf_test_messages.proto2.TestAllTypesProto2"
testAllTypesProto2AnyCodec
$ WK.standardWktRegistry
main :: IO ()
main = do
hSetBinaryMode stdin True
hSetBinaryMode stdout True
hSetBuffering stdout NoBuffering
hSetBuffering stderr LineBuffering
-- Proto2 extension JSON codecs: the loadProto splice generates
-- an ExtensionRegistry value per extend block; pass it via
-- Data.Reflection.give so the Generated ToJSON/FromJSON instances
-- can pick it up.
let _reg = registerExt_protobuf_test_messages_proto2_extension_int32
loop
where
loop = do
eof <- isEOF
if eof
then pure ()
else do
mLen <- readLE32 stdin
case mLen of
Nothing -> pure () -- short read at boundary; treat as EOF
Just len -> do
payload <- BS.hGet stdin (fromIntegral len)
if BS.length payload /= fromIntegral len
then pure () -- truncated payload; the runner exited mid-message
else do
resp <- case PD.decodeMessage payload of
Left e ->
pure
( runtimeErr
( "decode ConformanceRequest: "
<> T.pack (show e)
)
)
Right req -> evaluateOrCatch (handleRequest req)
writeResponseSafe resp
loop
{- | Catch every exception inside the handler so a single
malformed test case can't bring the runner down. The runner
treats a hung / killed test program as a hard failure across
the entire suite, which is far worse than a single 'runtime_error'.
-}
evaluateOrCatch :: IO ConformanceResponse -> IO ConformanceResponse
evaluateOrCatch act =
(act >>= evaluate)
`catch` \e -> do
hPutStrLn stderr ("wireform-conformance-runner: " <> show (e :: SomeException))
pure (runtimeErr (T.pack (show e)))
runtimeErr :: T.Text -> ConformanceResponse
runtimeErr t =
defaultConformanceResponse
{ conformanceResponseResult = Just (ConformanceResponse'Result'RuntimeError t)
}
{- | The non-WKT codec for the conformance suite's reference
message. The Any envelope inlines the message's JSON object
alongside @\@type@ rather than wrapping under @"value"@.
-}
testAllTypesProto3AnyCodec :: AnyCodec
testAllTypesProto3AnyCodec =
AnyCodec
{ acToJSON = \bs ->
case PD.decodeMessage bs :: Either PD.DecodeError TestAllTypesProto3 of
Left e -> Left ("Any embedded TestAllTypesProto3: " <> show e)
Right m -> Right (Aeson.toJSON m)
, acFromJSON = \v ->
case AesonT.parseEither (Aeson.parseJSON @TestAllTypesProto3) v of
Left e -> Left e
Right m -> Right (PE.encodeMessage m)
, acIsWkt = False
}
testAllTypesProto2AnyCodec :: AnyCodec
testAllTypesProto2AnyCodec =
AnyCodec
{ acToJSON = \bs ->
case PD.decodeMessage bs :: Either PD.DecodeError TestAllTypesProto2 of
Left e -> Left ("Any embedded TestAllTypesProto2: " <> show e)
Right m -> Right (Aeson.toJSON m)
, acFromJSON = \v ->
case AesonT.parseEither (Aeson.parseJSON @TestAllTypesProto2) v of
Left e -> Left e
Right m -> Right (PE.encodeMessage m)
, acIsWkt = False
}
writeResponse :: ConformanceResponse -> IO ()
writeResponse resp = do
let encoded = PE.encodeMessage resp
lenBytes = encodeLE32 (fromIntegral (BS.length encoded))
BS.hPut stdout lenBytes
BS.hPut stdout encoded
hFlush stdout
{- | Force-evaluate the encoded bytes so any lazy exception
thrown by the JSON / TextFormat encoders (e.g. WKT range
check 'error' calls) is caught here rather than killing the
process mid-write. Falls back to a 'runtime_error' response
so the upstream runner sees a clean reply.
-}
writeResponseSafe :: ConformanceResponse -> IO ()
writeResponseSafe resp =
writeResponse resp `catch` \e -> do
hPutStrLn
stderr
("wireform-conformance-runner (encode): " <> show (e :: SomeException))
writeResponse (runtimeErr (T.pack (show e)))
-- ---------------------------------------------------------------------------
-- Tiny LE32 helpers; matches what the upstream runner sends.
-- ---------------------------------------------------------------------------
readLE32 :: Handle -> IO (Maybe Word32)
readLE32 h = do
bs <- BS.hGet h 4
if BS.length bs /= 4
then pure Nothing
else
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 pure (Just (b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24)))
encodeLE32 :: Word32 -> BS.ByteString
encodeLE32 n =
BS.pack
[ fromIntegral (n .&. 0xFF)
, fromIntegral ((n `shiftR` 8) .&. 0xFF)
, fromIntegral ((n `shiftR` 16) .&. 0xFF)
, fromIntegral ((n `shiftR` 24) .&. 0xFF)
]