wireform-proto-0.2.0.0: test-integration/Test/Compat.hs
module Test.Compat (compatTests) where
import Data.Text (Text)
import Data.Text qualified as T
import Proto.Compat
import Proto.IDL.AST
import Proto.IDL.Parser
import Test.Syd
compatTests :: Spec
compatTests =
describe
"Schema Compatibility"
$ sequence_
[ describe
"BACKWARD compatibility"
$ sequence_
[ it "identical schemas are compatible" $ do
let schema = parseOrDie simpleSchema
assertCompatible (checkBackward schema schema)
, it "adding optional field is backward compatible" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
assertCompatible (checkBackward new old)
, it "removing field without reserving breaks backward" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
assertIncompatible (checkBackward new old)
assertHasRule "FIELD_REMOVED_NOT_RESERVED" (checkBackward new old)
, it "removing field with reservation is backward compatible" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; reserved 2; }"
]
assertCompatible (checkBackward new old)
, it "changing field type (incompatible wire) breaks backward" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { int32 value = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string value = 1; }"
]
assertIncompatible (checkBackward new old)
assertHasRule "FIELD_TYPE_CHANGED_INCOMPATIBLE" (checkBackward new old)
, it "changing between wire-compatible types warns" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { int32 value = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { int64 value = 1; }"
]
let result = checkBackward new old
assertCompatible result
assertHasRule "FIELD_TYPE_CHANGED_COMPATIBLE" result
, it "adding required field breaks backward" $ do
-- Simulate required by checking the rule directly
let fd = FieldDef () Nothing (Just Required) (FTScalar SInt32) "user_id" (FieldNumber 2) []
oldMsg = MessageDef () Nothing "Msg" [MEField (FieldDef () Nothing Nothing (FTScalar SString) "name" (FieldNumber 1) [])]
newMsg = MessageDef () Nothing "Msg" [MEField (FieldDef () Nothing Nothing (FTScalar SString) "name" (FieldNumber 1) []), MEField fd]
assertIncompatible (checkMessageCompat BackwardDir "Msg" newMsg oldMsg)
assertHasRule "REQUIRED_FIELD_ADDED" (checkMessageCompat BackwardDir "Msg" newMsg oldMsg)
]
, describe
"FORWARD compatibility"
$ sequence_
[ it "adding field is forward compatible (old ignores new)" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
assertCompatible (checkForward new old)
, it "removing field is forward compatible" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
assertCompatible (checkForward new old)
, it "type change (incompatible wire) breaks forward" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { int32 value = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { double value = 1; }"
]
assertIncompatible (checkForward new old)
]
, describe
"FULL compatibility"
$ sequence_
[ it "adding optional field is full compatible" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
assertCompatible (checkFull new old)
, it "removing field without reserving breaks full" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
assertIncompatible (checkFull new old)
]
, describe
"Enum compatibility"
$ sequence_
[ it "adding enum value warns for forward" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ACTIVE = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ACTIVE = 1; INACTIVE = 2; }"
]
let result = checkForward new old
assertCompatible result
assertHasRule "ENUM_VALUE_ADDED" result
, it "removing enum value breaks backward" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ACTIVE = 1; INACTIVE = 2; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ACTIVE = 1; }"
]
assertIncompatible (checkBackward new old)
assertHasRule "ENUM_VALUE_REMOVED" (checkBackward new old)
, it "renaming enum value warns" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ACTIVE = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status { UNKNOWN = 0; ENABLED = 1; }"
]
assertHasRule "ENUM_VALUE_RENAMED" (checkBackward new old)
]
, describe
"Transitive compatibility"
$ sequence_
[ it "checkCompatAll with multiple versions" $ do
let v1 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
v2 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; }"
]
v3 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 age = 2; bool active = 3; }"
]
assertCompatible (checkCompatAll BackwardTransitive v3 [v2, v1])
, it "transitive fails if any version is incompatible" $ do
let v1 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 value = 2; }"
]
v2 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; int32 value = 2; bool active = 3; }"
]
v3 =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; string value = 2; bool active = 3; }"
]
assertIncompatible (checkCompatAll BackwardTransitive v3 [v2, v1])
]
, describe
"NONE level"
$ sequence_
[ it "NONE always passes" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { double name = 1; }"
]
assertCompatible (checkCompat None new old)
]
, describe
"Field name changes"
$ sequence_
[ it "renaming a field warns" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string name = 1; }"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg { string full_name = 1; }"
]
assertHasRule "FIELD_NAME_CHANGED" (checkBackward new old)
]
, describe
"Complex schema evolution"
$ sequence_
[ it "safe evolution: add fields, reserve removed" $ do
let old =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message User {"
, " string name = 1;"
, " string email = 2;"
, " int32 age = 3;"
, "}"
]
new =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message User {"
, " string name = 1;"
, " string email = 2;"
, " reserved 3;"
, " string phone = 4;"
, " bool active = 5;"
, "}"
]
assertCompatible (checkFull new old)
]
]
-- Helpers
simpleSchema :: Text
simpleSchema =
T.unlines
[ "syntax = \"proto3\";"
, "message Msg {"
, " string name = 1;"
, " int32 value = 2;"
, "}"
]
parseOrDie :: Text -> ProtoFile
parseOrDie src = case parseProtoFile "<test>" src of
Left err -> error ("Parse failed: " <> show err)
Right pf -> pf
assertCompatible :: CompatResult -> IO ()
assertCompatible result =
(if (isCompatible result) then pure () else expectationFailure ("Expected compatible, got errors: " <> show (compatErrors result)))
assertIncompatible :: CompatResult -> IO ()
assertIncompatible result =
(not (isCompatible result)) `shouldBe` True
assertHasRule :: Text -> CompatResult -> IO ()
assertHasRule rule result =
(if (any (\e -> ceRule e == rule) (compatErrors result)) then pure () else expectationFailure ("Expected rule '" <> T.unpack rule <> "' in errors: " <> show (compatErrors result)))