packages feed

proto3-suite-0.9.4: tests/TestCodeGen.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module TestCodeGen where

import           ArbitraryGeneratedTestTypes    ()
import           Control.Applicative
import           Control.Monad
#ifdef SWAGGER
import qualified Data.Aeson
#endif
import qualified Data.ByteString.Lazy           as LBS
import           Data.Proxy                     (Proxy(..))
import           Data.String                    (IsString)
#ifdef SWAGGER
import           Data.Swagger                   (ToSchema)
import qualified Data.Swagger
#endif
import qualified Data.Text                      as T
import           Data.Typeable                  (Typeable, typeRep,
#ifdef SWAGGER
                                                 splitTyConApp, tyConName
#endif
                                                )
import           GHC.Exts                       (Proxy#, proxy#)
#ifdef SWAGGER
import           GHC.Stack                      (HasCallStack)
#endif
import           Google.Protobuf.Timestamp      (Timestamp(..))
import           Prelude                        hiding (FilePath)
import           Proto3.Suite.Class             (Message(..), def)
import           Proto3.Suite.DotProto          (fieldLikeName, prefixedEnumFieldName, typeLikeName)
import           Proto3.Suite.DotProto.AST      (DotProtoField(..), DotProtoIdentifier(..),
                                                 DotProtoType(..), DotProtoPrimType(..))
import           Proto3.Suite.DotProto.Generate
import           Proto3.Suite.Haskell.Parser    (Logger)
import           Proto3.Suite.JSONPB            (FromJSONPB (..), Options (..),
                                                 ToJSONPB (..), defaultOptions,
                                                 eitherDecode, encode,
                                                 jsonPBOptions)
import           Proto3.Suite.Types             (Enumerated(..))
import           System.Exit
import           Test.Proto.ToEncoder           (Iterator, Stripping)
import           Test.Tasty
import           Test.Tasty.HUnit               (testCase, (@?=))
import           Test.Tasty.QuickCheck          (Arbitrary, (===), testProperty)
import qualified Turtle
import qualified Turtle.Format                  as F
import qualified TestProto
import qualified TestProtoNegativeEnum
import qualified TestProtoOneof
import qualified TestProtoOptional
#ifdef SWAGGER
import qualified TestProtoWrappers
#endif

codeGenTests :: Logger -> TestTree
codeGenTests logger = testGroup "Code generator unit tests"
  [ dotProtoTests
  , jsonpbTests
  , hasDefaultTests
  , pascalCaseMessageNames
  , camelCaseMessageFieldNames
  , don'tAlterEnumFieldNames
  , knownTypeMessages
  , pythonInteroperation logger
#ifdef SWAGGER
  , swaggerTests
  , swaggerWrapperFormat
#endif
  ]

pythonInteroperation :: Logger -> TestTree
pythonInteroperation logger = testGroup "Python interoperation" $ do
  tt <- ["Data.Text.Lazy.Text", "Data.Text.Text", "Data.Text.Short.ShortText"]
  format <- ["Binary", "Jsonpb"]
  testEncode <- [True, False]
  direct <- [False, True]
  guard $ not direct || (testEncode && format == "Binary")
  let f = if testEncode then simpleEncodeDotProto direct else simpleDecodeDotProto
  pure @[] (f logger tt format)

#ifdef SWAGGER
swaggerWrapperFormat :: TestTree
swaggerWrapperFormat = testGroup "Swagger Wrapper Format"
    [ expectSchema @TestProtoWrappers.TestDoubleValue
           "{\"properties\":{\"wrapper\":{\"format\":\"DoubleValue\",\"type\":\"number\"},\"many\":{\"items\":{\"format\":\"double\",\"type\":\"number\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestDoubleValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"double\",\"type\":\"number\"},\"many\":{\"items\":{\"format\":\"double\",\"type\":\"number\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestDoubleValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestFloatValue
           "{\"properties\":{\"wrapper\":{\"format\":\"FloatValue\",\"type\":\"number\"},\"many\":{\"items\":{\"format\":\"float\",\"type\":\"number\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestFloatValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"float\",\"type\":\"number\"},\"many\":{\"items\":{\"format\":\"float\",\"type\":\"number\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestFloatValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestInt64Value
           "{\"properties\":{\"wrapper\":{\"format\":\"Int64Value\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestInt64ValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestInt64ValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestUInt64Value
           "{\"properties\":{\"wrapper\":{\"format\":\"UInt64Value\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int64\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestUInt64ValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"int64\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int64\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestUInt64ValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestInt32Value
           "{\"properties\":{\"wrapper\":{\"format\":\"Int32Value\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestInt32ValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestInt32ValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestUInt32Value
           "{\"properties\":{\"wrapper\":{\"format\":\"UInt32Value\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int32\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestUInt32ValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"int32\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"},\"many\":{\"items\":{\"format\":\"int32\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestUInt32ValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestBoolValue
           "{\"properties\":{\"wrapper\":{\"format\":\"BoolValue\",\"type\":\"boolean\"},\"many\":{\"items\":{\"type\":\"boolean\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestBoolValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"type\":\"boolean\"},\"many\":{\"items\":{\"type\":\"boolean\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestBoolValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestStringValue
           "{\"properties\":{\"wrapper\":{\"format\":\"StringValue\",\"type\":\"string\"},\"many\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestStringValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"type\":\"string\"},\"many\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestStringValuePickOne\"}},\"type\":\"object\"}"
    , expectSchema @TestProtoWrappers.TestBytesValue
           "{\"properties\":{\"wrapper\":{\"format\":\"BytesValue\",\"type\":\"string\"},\"many\":{\"items\":{\"format\":\"byte\",\"type\":\"string\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestBytesValuePickOne\"}},\"type\":\"object\"}"
           "{\"properties\":{\"wrapper\":{\"format\":\"byte\",\"type\":\"string\"},\"many\":{\"items\":{\"format\":\"byte\",\"type\":\"string\"},\"type\":\"array\"},\"pickOne\":{\"$ref\":\"#/definitions/TestBytesValuePickOne\"}},\"type\":\"object\"}"
    ]
  where
    expectSchema ::
      forall a .
      (ToSchema a, Typeable a, HasCallStack) =>
      LBS.ByteString ->
      LBS.ByteString ->
      TestTree
    expectSchema wrapperFormat noWrapperFormat =
        testCase (tyConName (fst (splitTyConApp (typeRep (Proxy @a))))) $ do
          lbsSchemaOf @a @?= (if wf then wrapperFormat else noWrapperFormat)
      where
        wf :: Bool
#ifdef SWAGGER_WRAPPER_FORMAT
        wf = True
#else
        wf = False
#endif
#endif

knownTypeMessages :: TestTree
knownTypeMessages =
  testGroup
    "KnownType custom codec"
    [ testCase "Timestamp rfc3339 json encoding"
        $ encode defaultOptions (Timestamp 0 0) @?= "\"1970-01-01T00:00:00Z\""
    , testCase "Timestamp rfc3339 json decoding"
        $ eitherDecode "\"1970-01-01T00:00:00Z\"" @?= Right (Timestamp 0 0)
    ]

pascalCaseMessageNames :: TestTree
pascalCaseMessageNames = testGroup "PascalCasing of message names"
  [ testCase "Capitalizes letters after underscores"
      $ typeLikeName "protocol_analysis" @?= Right "ProtocolAnalysis"

  , testCase "Preserves casing of interior letters"
      $ typeLikeName "analyze_HTTP" @?= Right "AnalyzeHTTP"

  , testCase "Handles non-alphanumeric characters after underscore"
      $ typeLikeName "analyze_http_2" @?= Right "AnalyzeHttp2"

  , testCase "Preserves one underscore in double underscore sequence"
      $ typeLikeName "Analyze__HTTP" @?= Right "Analyze_HTTP"

  , testCase "Handles names prefixed with underscore"
      $ typeLikeName "_message_name" @?= Right "XMessageName"

  , testCase "Preserves trailing underscore"
      $ typeLikeName "message_name_" @?= Right "MessageName_"
  ]

camelCaseMessageFieldNames :: TestTree
camelCaseMessageFieldNames = testGroup "camelCasing of field names"
  [ testCase "Preserves capitalization patterns"
      $ fieldLikeName "IP" @?= "ip"

  , testCase "Preserves underscores"
      $ fieldLikeName "IP_address" @?= "ip_address"
  ]

don'tAlterEnumFieldNames :: TestTree
don'tAlterEnumFieldNames
  = testGroup "Do not alter enumeration field names"
  $ tc <$> [ "fnord"
           , "FNORD"
           , "PascalCase"
           , "camelCase"
           , "VOCIFEROUS_SNAKE_CASE"
           , "snake_case"
           , "snake_case_"
           ]
  where
    enumName     = "MyEnum"
    tc fieldName = testCase fieldName $
        prefixedEnumFieldName enumName fieldName @?= (enumName <> fieldName)

setPythonPath :: IO ()
setPythonPath = Turtle.export "PYTHONPATH" .
  maybe pyTmpDir (\p -> pyTmpDir <> ":" <> p) =<< Turtle.need "PYTHONPATH"

simpleEncodeDotProto :: Bool -> Logger -> String -> T.Text -> TestTree
simpleEncodeDotProto direct logger chosenStringType format =
    testCase ("generate code for a simple .proto and then use it to encode messages" ++
              " with string type " ++ chosenStringType ++ " in format " ++ show format ++
              (if direct then ", direct mode" else ", intermediate mode"))
    $ do
         decodedStringType <- either die pure (parseStringType chosenStringType)

         compileTestDotProtos logger decodedStringType direct
         -- Compile our generated encoder
         let encodeCmd = "tests/encode.sh " <> hsTmpDir
                           <> (if direct then " -DTYPE_LEVEL_FORMAT" else "")
#if DHALL
                           <> " -DDHALL"
#endif
         Turtle.shell encodeCmd empty >>= (@?= ExitSuccess)

         -- The python test of encoding exits with a special error code to indicate
         -- all tests were successful.  When directly encoding without an intermediate
         -- data structure, we run the test several times (still compiling just once)
         -- in order to cover various supported ways of iterating over repeated fields.
         setPythonPath
         let iterators :: [Iterator]
             iterators
               | direct = [minBound .. maxBound]
               | otherwise = [minBound]  -- Just an unused placeholder
             strippings :: [Stripping]
             strippings
               | direct = [minBound .. maxBound]
               | otherwise = [minBound]  -- Just an unused placeholder
         forM_ iterators $ \(iterator :: Iterator) -> do
           forM_ strippings $ \(stripping :: Stripping) -> do
             when direct $ do
               putStrLn $ "        iterator: " ++ show iterator
               putStrLn $ "        stripping: " ++ show stripping
             let cmd = hsTmpDir <> "/simpleEncodeDotProto " <> format <>
                       " " <> T.pack (show iterator) <> " " <> T.pack (show stripping) <>
                       " | python tests/check_simple_dot_proto.py " <> format
             Turtle.shell cmd empty >>= (@?= ExitFailure 12)

         -- Not using bracket so that we can inspect the output to fix the tests
         Turtle.rmtree hsTmpDir
         Turtle.rmtree pyTmpDir

simpleDecodeDotProto :: Logger -> String -> T.Text -> TestTree
simpleDecodeDotProto logger chosenStringType format =
    testCase ("generate code for a simple .proto and then use it to decode messages" ++
              " with string type " ++ chosenStringType ++ " in format " ++ show format)
    $ do
         decodedStringType <- either die pure (parseStringType chosenStringType)

         compileTestDotProtos logger decodedStringType False
         -- Compile our generated decoder
         let decodeCmd = "tests/decode.sh " <> hsTmpDir
#if DHALL
                           <> " -DDHALL"
#endif
         Turtle.shell decodeCmd empty >>= (@?= ExitSuccess)

         setPythonPath
         let cmd = "python tests/send_simple_dot_proto.py " <> format <> " | FORMAT=" <> format <> " " <> hsTmpDir <> "/simpleDecodeDotProto "
         Turtle.shell cmd empty >>= (@?= ExitSuccess)

         -- Not using bracket so that we can inspect the output to fix the tests
         Turtle.rmtree hsTmpDir
         Turtle.rmtree pyTmpDir

-- * Helpers

hsTmpDir, pyTmpDir :: IsString a => a
hsTmpDir = "test-files/hs-tmp"
pyTmpDir = "test-files/py-tmp"

defaultStringType :: StringType
defaultStringType = StringType "Data.Text.Lazy" "Text"

compileTestDotProtos :: Logger -> StringType -> Bool -> IO ()
compileTestDotProtos logger decodedStringType typeLevel = do
  Turtle.mktree hsTmpDir
  Turtle.mktree pyTmpDir
  let protoFiles :: [Turtle.FilePath]
      protoFiles =
        [ "test_proto.proto"
        , "test_proto_import.proto"
        , "test_proto_negative_enum.proto"
        , "test_proto_oneof.proto"
        , "test_proto_oneof_import.proto"
        {- These tests have been temporarily removed to pass CI.
        , "test_proto_leading_dot.proto"
        , "test_proto_protoc_plugin.proto"
        -}
        , "test_proto_nested_message.proto"
        , "test_proto_wrappers.proto"
        , "test_proto_negative_enum.proto"
        , "test_proto_optional.proto"
        ]

  forM_ protoFiles $ \protoFile -> do
    compileDotProtoFileOrDie logger
        CompileArgs{ includeDir = ["test-files"]
                   , extraInstanceFiles = ["test-files" Turtle.</> "Orphan.hs"]
                   , outputDir = hsTmpDir
                   , inputProto = protoFile
                   , stringType = decodedStringType
                   , typeLevelFormat = typeLevel
                   }

    let cmd = T.concat [ "protoc --python_out="
                       , pyTmpDir
                       , " --proto_path=test-files"
                       , " test-files/" <> Turtle.format F.fp protoFile
                       ]
    Turtle.shell cmd empty >>= (@?= ExitSuccess)

  Turtle.touch (pyTmpDir Turtle.</> "__init__.py")

dotProtoTests :: TestTree
dotProtoTests = testGroup "dotProto method tests"
  [ dotProtoTest @TestProto.Trivial
      [ DotProtoField 1 (Prim Int32) (Single "trivialField") [] ""
      ]
  , dotProtoTest @TestProto.MultipleFields
      [ DotProtoField 1 (Prim Double) (Single "multiFieldDouble") [] ""
      , DotProtoField 2 (Prim Float) (Single "multiFieldFloat") [] ""
      , DotProtoField 3 (Prim Int32) (Single "multiFieldInt32") [] ""
      , DotProtoField 4 (Prim Int64) (Single "multiFieldInt64") [] ""
      , DotProtoField 5 (Prim String) (Single "multiFieldString") [] ""
      , DotProtoField 6 (Prim Bool) (Single "multiFieldBool") [] ""
      ]
  , dotProtoTest @TestProto.SignedInts
      [ DotProtoField 1 (Prim SInt32) (Single "signed32") [] ""
      , DotProtoField 2 (Prim SInt64) (Single "signed64") [] ""
      ]
  , dotProtoTest @TestProto.MapTest
      [ DotProtoField 1 (Map String SInt32) (Single "prim") [] ""
      , DotProtoField 2 (Map Int32 (Named (Single "WrappedTrivial"))) (Single "trivial") [] ""
      , DotProtoField 3 (Map SInt32 SInt32) (Single "signed") [] ""
          -- Current parser discards comments.
      ]
  , dotProtoTest @TestProto.WithNestingRepeatedInts
      [ DotProtoField 1 (Repeated (Named (Single "NestedInts"))) (Single "nestedInts") [] ""
      ]
  , dotProtoTest @TestProto.WithRepeatedSigned
      [ DotProtoField 1 (Repeated SInt32) (Single "r32") [] ""
      , DotProtoField 2 (Repeated SInt64) (Single "r64") [] ""
      ]
  , dotProtoTest @TestProtoOptional.WithOptional
      [ DotProtoField 10 (Optional Double) (Single "optionalDouble") [] ""
      , DotProtoField 20 (Optional Float) (Single "optionalFloat") [] ""
      , DotProtoField 30 (Optional Int32) (Single "optionalInt32") [] ""
      , DotProtoField 40 (Optional Int64) (Single "optionalInt64") [] ""
      , DotProtoField 50 (Optional UInt32) (Single "optionalUint32") [] ""
      , DotProtoField 60 (Optional UInt64) (Single "optionalUint64") [] ""
      , DotProtoField 70 (Optional SInt32) (Single "optionalSint32") [] ""
      , DotProtoField 80 (Optional SInt64) (Single "optionalSint64") [] ""
      , DotProtoField 90 (Optional Fixed32) (Single "optionalFixed32") [] ""
      , DotProtoField 100 (Optional Fixed64) (Single "optionalFixed64") [] ""
      , DotProtoField 110 (Optional SFixed32) (Single "optionalSfixed32") [] ""
      , DotProtoField 120 (Optional SFixed64) (Single "optionalSfixed64") [] ""
      , DotProtoField 130 (Optional Bool) (Single "optionalBool") [] ""
      , DotProtoField 140 (Optional String) (Single "optionalString") [] ""
      , DotProtoField 150 (Optional Bytes) (Single "optionalBytes") [] ""
      , DotProtoField 160 (Optional (Named (Single "Enum"))) (Single "optionalEnum") [] ""
      , DotProtoField 170 (Optional (Named (Single "Submessage"))) (Single "optionalSubmessage") [] ""
      ]
  ]

jsonpbTests :: TestTree
jsonpbTests = testGroup "JSONPB tests"
  [ testGroup "Round-trip tests"
      [ roundTripTest @TestProto.Trivial
      , roundTripTest @TestProto.MultipleFields
      , roundTripTest @TestProto.SignedInts
      , testProperty "roundTrip (SignedInts minBound minBound)" $
          roundTrip (TestProto.SignedInts minBound minBound)
      , testProperty "roundTrip (SignedInts maxBound maxBound)" $
          roundTrip (TestProto.SignedInts maxBound maxBound)
      , testProperty "roundTrip . WithEnum . Enumerated . Right" $
          roundTrip . TestProto.WithEnum . Enumerated . Right
      , roundTripTest @TestProto.WithNesting
      , roundTripTest @TestProto.WithNestingRepeated
      , roundTripTest @TestProto.WithNestingRepeatedInts
      , roundTripTest @TestProto.WithBytes
      , roundTripTest @TestProto.OutOfOrderFields
      , roundTripTest @TestProto.UsingImported
      , roundTripTest @TestProto.Wrapped
      , roundTripTest @TestProtoOneof.Something
      , roundTripTest @TestProtoOneof.WithImported
      , roundTripTest @TestProtoNegativeEnum.WithNegativeEnum
      , roundTripTest @TestProtoOptional.WithOptional
      ]
  , testGroup "Specific encoding tests" $
      let jsonPB = jsonPBOptions
          json = defaultOptions
      in
      [ encodesAs jsonPB (TestProto.MultipleFields 0 0 0 0 "" False)                                "{}"
      , encodesAs json   (TestProto.MultipleFields 0 2.0 0 0 "" True)                               "{\"multiFieldDouble\":0.0,\"multiFieldFloat\":2.0,\"multiFieldInt32\":0,\"multiFieldInt64\":\"0\",\"multiFieldString\":\"\",\"multiFieldBool\":true}"
      , encodesAs jsonPB (TestProto.SignedInts minBound minBound)
          "{\"signed32\":-2147483648,\"signed64\":\"-9223372036854775808\"}"
      , encodesAs jsonPB (TestProto.SignedInts maxBound maxBound)
          "{\"signed32\":2147483647,\"signed64\":\"9223372036854775807\"}"
      , encodesAs jsonPB (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM1)))
          "{}"
      , encodesAs json   (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM1)))
          "{\"enumField\":\"ENUM1\"}"
      , encodesAs jsonPB (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM3)))
          "{\"enumField\":\"ENUM3\"}"
      , encodesAs jsonPB (TestProto.WithNesting $ Just $ TestProto.WithNesting_Nested "" 0 [1,2] [66,99])
          "{\"nestedMessage\":{\"nestedPacked\":[1,2],\"nestedUnpacked\":[66,99]}}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneName "")))
          "{\"value\":\"42\",\"another\":99,\"name\":\"\"}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneSomeid 0)))
          "{\"value\":\"42\",\"another\":99,\"someid\":0}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg1 (TestProtoOneof.DummyMsg 66))))
          "{\"value\":\"42\",\"another\":99,\"dummyMsg1\":{\"dummy\":66}}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg2 (TestProtoOneof.DummyMsg 67))))
          "{\"value\":\"42\",\"another\":99,\"dummyMsg2\":{\"dummy\":67}}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyEnum (Enumerated (Right TestProtoOneof.DummyEnumDUMMY0)))))
          "{\"value\":\"42\",\"another\":99,\"dummyEnum\":\"DUMMY0\"}"
      , encodesAs jsonPB (TestProtoOneof.Something 42 99 Nothing)
          "{\"value\":\"42\",\"another\":99}"
      , encodesAs json   (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneName "")))
          "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"name\":\"\"}}"
      , encodesAs json   (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneSomeid 0)))
          "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"someid\":0}}"
      , encodesAs json   (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg1 (TestProtoOneof.DummyMsg 66))))
          "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyMsg1\":{\"dummy\":66}}}"
      , encodesAs json   (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg2 (TestProtoOneof.DummyMsg 67))))
          "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyMsg2\":{\"dummy\":67}}}"
      , encodesAs json   (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyEnum (Enumerated (Right TestProtoOneof.DummyEnumDUMMY0)))))
          "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyEnum\":\"DUMMY0\"}}"
      , encodesAs json   (TestProtoOneof.Something 42 99 Nothing)
          "{\"value\":\"42\",\"another\":99,\"pickOne\":null}"
      ]
  , testGroup "Specific decoding tests" $
      [ decodesAs "{\"signed32\":2147483647,\"signed64\":\"9223372036854775807\"}"
          (TestProto.SignedInts 2147483647 9223372036854775807)
      , decodesAs "{\"enumField\":\"ENUM3\"}"                                             (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM3)))
      , decodesAs "{\"enumField\":null}"
          (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM1)))
      , decodesAs "{}"
          (TestProto.WithEnum (Enumerated (Right TestProto.WithEnum_TestEnumENUM1)))
      , decodesAs "{\"nestedMessage\":{}}"
          (TestProto.WithNesting $ Just $ TestProto.WithNesting_Nested "" 0 [] [])
      , testGroup "JSONPB"
          [ decodesAs "{\"value\":\"42\",\"another\":99,\"someid\":66}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneSomeid 66)))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"name\":\"foo\"}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneName "foo")))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"dummyMsg1\":{\"dummy\":41}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg1 (TestProtoOneof.DummyMsg 41))))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"dummyMsg2\":{\"dummy\":43}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg2 (TestProtoOneof.DummyMsg 43))))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"dummyEnum\":\"DUMMY0\"}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyEnum (Enumerated (Right TestProtoOneof.DummyEnumDUMMY0)))))
          , decodesAs "{\"value\":\"42\",\"another\":99}"
              (TestProtoOneof.Something 42 99 Nothing)
          ]
      , testGroup "JSON"
          [ decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"name\":\"\"}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneName "")))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"someid\":0}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneSomeid 0)))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyMsg1\":{\"dummy\":66}}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg1 (TestProtoOneof.DummyMsg 66))))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyMsg2\":{\"dummy\":67}}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyMsg2 (TestProtoOneof.DummyMsg 67))))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{\"dummyEnum\":\"DUMMY0\"}}"
              (TestProtoOneof.Something 42 99 (Just (TestProtoOneof.SomethingPickOneDummyEnum (Enumerated (Right TestProtoOneof.DummyEnumDUMMY0)))))
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":{}}"                      (TestProtoOneof.Something 42 99 Nothing)
          , decodesAs "{\"value\":\"42\",\"another\":99,\"pickOne\":null}"                    (TestProtoOneof.Something 42 99 Nothing)
          ]
      ]
  ]

#ifdef SWAGGER
swaggerTests :: TestTree
swaggerTests = testGroup "Swagger tests"
  [ schemaOf @TestProtoOneof.Something
      "{\"properties\":{\"value\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"another\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"pickOne\":{\"$ref\":\"#/definitions/SomethingPickOne\"}},\"type\":\"object\"}"
  , schemaOf @TestProtoOneof.SomethingPickOne
      "{\"properties\":{\"name\":{\"type\":\"string\"},\"someid\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"},\"dummyMsg1\":{\"$ref\":\"#/definitions/DummyMsg\"},\"dummyMsg2\":{\"$ref\":\"#/definitions/DummyMsg\"},\"dummyEnum\":{\"$ref\":\"#/definitions/DummyEnum\"}},\"maxProperties\":1,\"minProperties\":1,\"type\":\"object\"}"
  , schemaOf @TestProtoOneof.DummyMsg
      "{\"properties\":{\"dummy\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"}},\"type\":\"object\"}"
  , schemaOf @(Enumerated TestProtoOneof.DummyEnum)
      "{\"enum\":[\"DUMMY0\",\"DUMMY1\"],\"type\":\"string\"}"
  , schemaOf @TestProtoOptional.WithOptional $
      "{\"properties\":{" <>
      "\"optionalDouble\":{\"format\":\"double\",\"type\":\"number\"}," <>
      "\"optionalFloat\":{\"format\":\"float\",\"type\":\"number\"}," <>
      "\"optionalInt32\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"}," <>
      "\"optionalInt64\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"}," <>
      "\"optionalUint32\":{\"format\":\"int32\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"}," <>
      "\"optionalUint64\":{\"format\":\"int64\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"}," <>
      "\"optionalSint32\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"}," <>
      "\"optionalSint64\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"}," <>
      "\"optionalFixed32\":{\"format\":\"int32\",\"maximum\":4294967295,\"minimum\":0,\"type\":\"integer\"}," <>
      "\"optionalFixed64\":{\"format\":\"int64\",\"maximum\":18446744073709551615,\"minimum\":0,\"type\":\"integer\"}," <>
      "\"optionalSfixed32\":{\"format\":\"int32\",\"maximum\":2147483647,\"minimum\":-2147483648,\"type\":\"integer\"}," <>
      "\"optionalSfixed64\":{\"format\":\"int64\",\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"}," <>
      "\"optionalBool\":{\"type\":\"boolean\"}," <>
      "\"optionalString\":{\"type\":\"string\"}," <>
      "\"optionalBytes\":{\"format\":\"byte\",\"type\":\"string\"}," <>
      "\"optionalEnum\":{\"$ref\":\"#/definitions/Enum\"}," <>
      "\"optionalSubmessage\":{\"$ref\":\"#/definitions/Submessage\"}" <>
      "},\"type\":\"object\"}"
  ]
#endif

hasDefaultTests :: TestTree
hasDefaultTests = testGroup "Generic HasDefault"
  [ testProperty "MultipleFields" $
      (def :: TestProto.MultipleFields) ===
        TestProto.MultipleFields {multipleFieldsMultiFieldDouble = 0.0, multipleFieldsMultiFieldFloat = 0.0, multipleFieldsMultiFieldInt32 = 0, multipleFieldsMultiFieldInt64 = 0, multipleFieldsMultiFieldString = "", multipleFieldsMultiFieldBool = False}
  , testProperty "WithNesting" $
      (def :: TestProto.WithNesting) ===
        TestProto.WithNesting {withNestingNestedMessage = Nothing}
  , testProperty "WithNestingRepeated" $
      (def :: TestProto.WithNestingRepeated) ===
        TestProto.WithNestingRepeated {withNestingRepeatedNestedMessages = []}
  , testProperty "WithEnum" $
      (def :: TestProto.WithEnum) ===
        TestProto.WithEnum {withEnumEnumField = Enumerated {enumerated = Right TestProto.WithEnum_TestEnumENUM1}}
  ]

-- * Helper quickcheck props

dotProtoTest ::
  forall a .
  (Message a, Typeable a) =>
  [DotProtoField] ->
  TestTree
dotProtoTest expected =
  testProperty ("dotProtoTest @" ++ show (typeRep (Proxy :: Proxy a))) $
    dotProto (proxy# :: Proxy# a) === expected

roundTripTest ::
  forall a .
  (ToJSONPB a, FromJSONPB a, Eq a, Arbitrary a, Show a, Typeable a) =>
  TestTree
roundTripTest =
  testProperty ("roundTripTest @" ++ show (typeRep (Proxy :: Proxy a))) $
    roundTrip @a

roundTrip :: (ToJSONPB a, FromJSONPB a, Eq a)
          => a -> Bool
roundTrip x = roundTrip' False && roundTrip' True
  where
    roundTrip' emitDefaults =
      eitherDecode (encode defaultOptions{ optEmitDefaultValuedFields = emitDefaults} x)
      ==
      Right x

encodesAs ::
  forall a .
  (ToJSONPB a, Eq a, Show a, Typeable a) =>
  Options -> a -> LBS.ByteString -> TestTree
encodesAs opts x bs = testProperty (testName "") (encode opts x === bs)
  where
    testName =
      showString "encode @" .
      showsPrec 11 (typeRep (Proxy :: Proxy a)) .
      showChar ' ' .
      showsPrec 11 opts .
      showChar ' ' .
      showsPrec 11 x .
      showString " == " .
      showsPrec 5 bs

decodesAs ::
  forall a .
  (FromJSONPB a, Eq a, Show a, Typeable a) =>
  LBS.ByteString -> a -> TestTree
decodesAs bs x = testProperty (testName "")  (eitherDecode bs === Right x)
  where
    testName =
      showString "eitherDecode @" .
      showsPrec 11 (typeRep (Proxy :: Proxy a)) .
      showChar ' ' .
      showsPrec 11 bs .
      showString " == Right " .
      showsPrec 11 x

#ifdef SWAGGER
schemaOf ::
  forall a .
  (ToSchema a, Eq a, Show a, Typeable a) =>
  LBS.ByteString -> TestTree
schemaOf bs = testProperty (testName "") (lbsSchemaOf @a === bs)
  where
    testName =
      showString "lbsSchemaOf @" .
      showsPrec 11 (typeRep (Proxy :: Proxy a)) .
      showString " == " .
      showsPrec 5 bs

lbsSchemaOf :: forall a . ToSchema a => LBS.ByteString
lbsSchemaOf = Data.Aeson.encode (Data.Swagger.toSchema (Proxy @a))
#endif