wireform-proto-0.1.0.0: test-integration/Test/Editions.hs
module Test.Editions (editionsTests) where
import Data.Map.Strict qualified as Map
import Data.Maybe (isJust)
import Data.Text qualified as T
import Test.Tasty
import Test.Tasty.HUnit
import Proto.CodeGen
import Proto.IDL.AST
import Proto.IDL.Parser (parseProtoFile)
import Proto.IDL.Parser.Resolver (ResolvedProto (..))
editionsTests :: TestTree
editionsTests =
testGroup
"Proto editions"
[ featureResolutionTests
, fieldPresenceTests
, repeatedEncodingTests
, enumTypeTests
, codegenOutputTests
]
-- ---------------------------------------------------------------------------
-- Feature resolution: applyFeatureOptions / resolveFileFeatures / resolveFieldFeatures
-- ---------------------------------------------------------------------------
featureResolutionTests :: TestTree
featureResolutionTests =
testGroup
"Feature resolution"
[ testCase "edition 2023 defaults to proto3 semantics" $ do
let fs = featuresForEdition (Edition "2023")
featureFieldPresence fs @?= ExplicitPresence
featureEnumType fs @?= OpenEnum
featureRepeatedFieldEncoding fs @?= PackedEncoding
featureUtf8Validation fs @?= Utf8Verify
featureMessageEncoding fs @?= LengthPrefixedEncoding
featureJsonFormat fs @?= JsonAllow
, testCase "applyFeatureOptions: field_presence IMPLICIT" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "field_presence"])
(CIdent "IMPLICIT")
]
fs = applyFeatureOptions opts defaultFeatureSet
featureFieldPresence fs @?= ImplicitPresence
, testCase "applyFeatureOptions: field_presence EXPLICIT" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "field_presence"])
(CIdent "EXPLICIT")
]
fs = applyFeatureOptions opts defaultFeatureSet
featureFieldPresence fs @?= ExplicitPresence
, testCase "applyFeatureOptions: field_presence LEGACY_REQUIRED" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "field_presence"])
(CIdent "LEGACY_REQUIRED")
]
fs = applyFeatureOptions opts defaultFeatureSet
featureFieldPresence fs @?= LegacyRequired
, testCase "applyFeatureOptions: enum_type CLOSED" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "enum_type"])
(CIdent "CLOSED")
]
fs = applyFeatureOptions opts defaultFeatureSet
featureEnumType fs @?= ClosedEnum
, testCase "applyFeatureOptions: repeated_field_encoding EXPANDED" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "repeated_field_encoding"])
(CIdent "EXPANDED")
]
fs = applyFeatureOptions opts defaultFeatureSet
featureRepeatedFieldEncoding fs @?= ExpandedEncoding
, testCase "applyFeatureOptions: unknown options are silently ignored" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "nonexistent_feature"])
(CIdent "SOME_VALUE")
]
fs = applyFeatureOptions opts defaultFeatureSet
fs @?= defaultFeatureSet
, testCase "resolveFileFeatures: non-editions returns defaultFeatureSet" $ do
let fs3 = resolveFileFeatures Proto3 []
fs2 = resolveFileFeatures Proto2 []
fs3 @?= defaultFeatureSet
fs2 @?= defaultFeatureSet
, testCase "resolveFileFeatures: edition 2023 with override" $ do
let opts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "repeated_field_encoding"])
(CIdent "EXPANDED")
]
fs = resolveFileFeatures (Editions (Edition "2023")) opts
featureRepeatedFieldEncoding fs @?= ExpandedEncoding
-- Other features stay at edition 2023 defaults
featureFieldPresence fs @?= ExplicitPresence
, testCase "resolveFieldFeatures: field overrides parent" $ do
let parent = defaultFeatureSet { featureEnumType = OpenEnum }
fieldOpts =
[ OptionDef
()
(OptionName [SimpleOption "features", SimpleOption "enum_type"])
(CIdent "CLOSED")
]
fs = resolveFieldFeatures parent fieldOpts
featureEnumType fs @?= ClosedEnum
-- Non-overridden features stay from parent
featureFieldPresence fs @?= featureFieldPresence parent
]
-- ---------------------------------------------------------------------------
-- Field presence normalization (applyEditionFieldPresence)
-- ---------------------------------------------------------------------------
fieldPresenceTests :: TestTree
fieldPresenceTests =
testGroup
"field_presence normalization"
[ testCase "non-editions files are unmodified" $ do
let src = "syntax = \"proto3\"; message M { int32 x = 1; }"
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf ->
let pf' = applyEditionFieldPresence pf
in protoTopLevels pf' @?= protoTopLevels pf
, testCase "editions EXPLICIT presence → Optional label" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "option features.field_presence = EXPLICIT;"
, "message M { int32 x = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
fieldLabelOf pf' "x" @?= Just Optional
, testCase "editions IMPLICIT presence → Nothing (plain singular)" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "option features.field_presence = IMPLICIT;"
, "message M { int32 x = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
fieldLabelOf pf' "x" @?= Nothing
, testCase "editions LEGACY_REQUIRED → Required label" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "option features.field_presence = LEGACY_REQUIRED;"
, "message M { int32 x = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
fieldLabelOf pf' "x" @?= Just Required
, testCase "repeated fields are unaffected by field_presence" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "option features.field_presence = EXPLICIT;"
, "message M { repeated int32 xs = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
fieldLabelOf pf' "xs" @?= Just Repeated
, testCase "per-field override takes precedence over file default" $ do
-- File is IMPLICIT, but this specific field is EXPLICIT
let src = T.unlines
[ "edition = \"2023\";"
, "option features.field_presence = IMPLICIT;"
, "message M {"
, " int32 x = 1;"
, " int32 y = 2 [features.field_presence = EXPLICIT];"
, "}"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
fieldLabelOf pf' "x" @?= Nothing -- IMPLICIT
fieldLabelOf pf' "y" @?= Just Optional -- field-level EXPLICIT
]
where
fieldLabelOf :: ProtoFile -> T.Text -> Maybe FieldLabel
fieldLabelOf pf fname =
let findField [] = Nothing
findField (TLMessage msg : _) =
let fields = [fd | MEField fd <- msgElements msg, fieldName fd == fname]
in case fields of
(fd : _) -> Just (fieldLabel fd)
[] -> Nothing
findField (_ : rest) = findField rest
in case findField (protoTopLevels pf) of
Just ml -> ml
Nothing -> Nothing
-- ---------------------------------------------------------------------------
-- repeated_field_encoding in the text codegen path
-- ---------------------------------------------------------------------------
repeatedEncodingTests :: TestTree
repeatedEncodingTests =
testGroup
"repeated_field_encoding in codegen"
[ testCase "default opts: repeated scalar uses packed encoding" $ do
let src = "syntax = \"proto3\"; package t; message M { repeated int32 xs = 1; }"
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf
assertBool "packed encoder present" (T.isInfixOf "encodePackedInt32" code || T.isInfixOf "encodePacked" code)
, testCase "genPackedRepeated=False: repeated scalar uses expanded encoding" $ do
let src = "syntax = \"proto3\"; package t; message M { repeated int32 xs = 1; }"
opts = defaultGenerateOpts { genPackedRepeated = False }
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry opts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText opts reg "<test>" pf
-- Expanded encoding uses foldl' per element
assertBool "expanded foldl present" (T.isInfixOf "foldl'" code)
-- Should NOT call a pack function
assertBool "no packed encoder" (not (T.isInfixOf "encodePacked" code))
, testCase "edition 2023 EXPANDED override uses expanded encoding" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "package t;"
, "option features.repeated_field_encoding = EXPANDED;"
, "message M { repeated int32 xs = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf
assertBool "expanded foldl present" (T.isInfixOf "foldl'" code)
assertBool "no packed encoder" (not (T.isInfixOf "encodePacked" code))
]
-- ---------------------------------------------------------------------------
-- enum_type=CLOSED in codegen
-- ---------------------------------------------------------------------------
enumTypeTests :: TestTree
enumTypeTests =
testGroup
"enum_type in codegen"
[ testCase "default (OPEN): enum decode uses decodeFieldEnum" $ do
let src = T.unlines
[ "syntax = \"proto3\";"
, "package t;"
, "enum E { E_ZERO = 0; E_ONE = 1; }"
, "message M { E e = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf
assertBool "open enum uses decodeFieldEnum" (T.isInfixOf "decodeFieldEnum" code)
, testCase "genClosedEnums=True: enum decode uses fromProtoEnum + decodeFail" $ do
let src = T.unlines
[ "syntax = \"proto3\";"
, "package t;"
, "enum E { E_ZERO = 0; E_ONE = 1; }"
, "message M { E e = 1; }"
]
opts = defaultGenerateOpts { genClosedEnums = True }
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry opts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText opts reg "<test>" pf
assertBool "closed enum uses fromProtoEnum" (T.isInfixOf "fromProtoEnum" code)
assertBool "closed enum uses decodeFail" (T.isInfixOf "decodeFail" code)
assertBool "closed enum does not use decodeFieldEnum" (not (T.isInfixOf "decodeFieldEnum" code))
, testCase "edition 2023 CLOSED override generates closed decode" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "package t;"
, "option features.enum_type = CLOSED;"
, "enum E { E_ZERO = 0; E_ONE = 1; }"
, "message M { E e = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure (show e)
Right pf -> do
let reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf
assertBool "edition closed enum uses fromProtoEnum" (T.isInfixOf "fromProtoEnum" code)
assertBool "edition closed enum uses decodeFail" (T.isInfixOf "decodeFail" code)
]
-- ---------------------------------------------------------------------------
-- General codegen output checks for editions
-- ---------------------------------------------------------------------------
codegenOutputTests :: TestTree
codegenOutputTests =
testGroup
"Edition codegen output"
[ testCase "edition = 2023 parses and generates a module" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "package test.ed;"
, "message Person { string name = 1; int32 age = 2; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure ("parse failed: " <> show e)
Right pf -> do
let reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf
assertBool "data Person" (T.isInfixOf "data Person" code)
assertBool "MessageEncode instance" (T.isInfixOf "MessageEncode Person" code)
assertBool "MessageDecode instance" (T.isInfixOf "MessageDecode Person" code)
, testCase "edition EXPLICIT presence generates Maybe field" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "package test.ed;"
, "option features.field_presence = EXPLICIT;"
, "message M { int32 x = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure ("parse failed: " <> show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf' "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf'
assertBool "Maybe field for EXPLICIT presence" (T.isInfixOf "Maybe" code)
, testCase "edition IMPLICIT presence generates plain (non-Maybe) scalar field" $ do
let src = T.unlines
[ "edition = \"2023\";"
, "package test.ed;"
, "option features.field_presence = IMPLICIT;"
, "message M { int32 x = 1; }"
]
case parseProtoFile "<test>" src of
Left e -> assertFailure ("parse failed: " <> show e)
Right pf -> do
let pf' = applyEditionFieldPresence pf
reg = buildTypeRegistry defaultGenerateOpts [("<test>", ResolvedProto pf' "<test>" Map.empty)]
code = generateModuleText defaultGenerateOpts reg "<test>" pf'
-- x :: !Int32 (not Maybe)
assertBool "plain Int32 for IMPLICIT presence" (T.isInfixOf "Int32" code)
-- The field should NOT be Maybe-wrapped (just a bare Int32 field)
let lines' = T.lines code
xLine = filter (\l -> T.isInfixOf "mX " l || T.isInfixOf ":: !Int32" l) lines'
assertBool "non-Maybe scalar field" (not (null xLine))
]