wireform-proto-0.2.0.0: test-integration/Test/PrintInspect.hs
module Test.PrintInspect (printInspectTests) where
import Data.Maybe (isJust, isNothing)
import Data.Text (Text)
import Data.Text qualified as T
import Proto.IDL.AST
import Proto.IDL.Inspect
import Proto.IDL.Parser
import Proto.IDL.Print
import Test.Syd
printInspectTests :: Spec
printInspectTests =
describe
"Print & Inspect"
$ sequence_
[ describe
"Exact printing"
$ sequence_
[ it "roundtrip: parse -> print -> parse yields same AST" $ do
let original = complexProto
case parseProtoFile "<test>" original of
Left e -> expectationFailure (show e)
Right ast1 -> do
let printed = printProtoFile ast1
case parseProtoFile "<printed>" printed of
Left e -> expectationFailure ("Re-parse failed: " <> show e <> "\n\nPrinted:\n" <> T.unpack printed)
Right ast2 -> ast1 `shouldBe` ast2
, it "syntax declaration" $ do
let ast = ProtoFile Proto3 Nothing [] [] [] Nothing
T.isInfixOf "syntax = \"proto3\";" (printProtoFile ast) `shouldBe` True
, it "syntax proto2" $ do
let ast = ProtoFile Proto2 Nothing [] [] [] Nothing
T.isInfixOf "syntax = \"proto2\";" (printProtoFile ast) `shouldBe` True
, it "package declaration" $ do
let ast = ProtoFile Proto3 (Just "my.package") [] [] [] Nothing
T.isInfixOf "package my.package;" (printProtoFile ast) `shouldBe` True
, it "import" $ do
let ast = ProtoFile Proto3 Nothing [ImportDef () Nothing "other.proto"] [] [] Nothing
T.isInfixOf "import \"other.proto\";" (printProtoFile ast) `shouldBe` True
, it "public import" $ do
let ast = ProtoFile Proto3 Nothing [ImportDef () (Just ImportPublic) "other.proto"] [] [] Nothing
T.isInfixOf "import public \"other.proto\";" (printProtoFile ast) `shouldBe` True
, it "simple message roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " string name = 1;"
, " int32 value = 2;"
, "}"
]
roundtripTest src
, it "enum roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "enum Status {"
, " UNKNOWN = 0;"
, " ACTIVE = 1;"
, "}"
]
roundtripTest src
, it "service roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "service Greeter {"
, " rpc SayHello (HelloReq) returns (HelloReply);"
, "}"
]
roundtripTest src
, it "map field roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " map<string, int32> labels = 1;"
, "}"
]
roundtripTest src
, it "oneof roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " oneof val {"
, " string text = 1;"
, " int32 number = 2;"
, " }"
, "}"
]
roundtripTest src
, it "reserved roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " reserved 2, 10 to 20;"
, " reserved \"old_field\";"
, "}"
]
roundtripTest src
, it "field options roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " string name = 1 [deprecated = true];"
, "}"
]
roundtripTest src
, it "option with extension name roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "option (my_custom_opt) = true;"
, "message Foo {"
, " string name = 1;"
, "}"
]
roundtripTest src
, it "nested message roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message Outer {"
, " message Inner {"
, " int32 x = 1;"
, " }"
, " Inner inner = 1;"
, "}"
]
roundtripTest src
, it "streaming rpc roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "service Chat {"
, " rpc Stream (stream Msg) returns (stream Msg);"
, "}"
]
roundtripTest src
, it "proto2 group roundtrip (desugared to message + field)" $ do
let src =
T.unlines
[ "syntax = \"proto2\";"
, "message M {"
, " repeated group Result = 1 {"
, " optional int32 x = 1;"
, " }"
, "}"
]
roundtripTest src
, it "extensions with options roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto2\";"
, "message M {"
, " extensions 4 to 8 [verification = UNVERIFIED];"
, "}"
]
roundtripTest src
, it "nested extend roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto2\";"
, "message M {"
, " extend N {"
, " optional int32 e = 100;"
, " }"
, "}"
]
roundtripTest src
, it "aggregate with list / extension-key values roundtrip" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "option (x) = { tags: [\"a\", \"b\"] };"
, "option (y) = { [foo.bar]: 1 };"
]
roundtripTest src
, it "editions reserved identifiers roundtrip" $ do
let src =
T.unlines
[ "edition = \"2023\";"
, "message M {"
, " reserved foo, bar;"
, "}"
]
roundtripTest src
, it "editions reserved identifiers print unquoted" $ do
let src =
T.unlines
[ "edition = \"2023\";"
, "message M {"
, " reserved foo, bar;"
, "}"
]
case parseProtoFile "<test>" src of
Left e -> expectationFailure (show e)
Right pf -> do
let printed = printProtoFile pf
T.isInfixOf "reserved foo, bar;" printed `shouldBe` True
T.isInfixOf "\"foo\"" printed `shouldBe` False
, it "proto3 reserved names still print quoted" $ do
let src =
T.unlines
[ "syntax = \"proto3\";"
, "message M {"
, " reserved \"foo\", \"bar\";"
, "}"
]
case parseProtoFile "<test>" src of
Left e -> expectationFailure (show e)
Right pf -> do
let printed = printProtoFile pf
T.isInfixOf "reserved \"foo\", \"bar\";" printed `shouldBe` True
]
, describe
"Exact printing (byte-for-byte)"
$ sequence_
[ it "reproduces a complex file byte-for-byte" $
exactPrintTest complexProto
, it "preserves a trailing ';' after a message block" $
exactPrintTest $
T.unlines
[ "syntax = \"proto3\";"
, "message Foo {"
, " string name = 1;"
, "};"
]
, it "preserves a trailing ';' after enum and service blocks" $
exactPrintTest $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status {"
, " UNKNOWN = 0;"
, "};"
, "service Greeter {"
, " rpc SayHello (Req) returns (Resp);"
, "};"
]
, it "preserves stray ';' between and inside declarations" $
exactPrintTest $
T.unlines
[ "syntax = \"proto3\";"
, ";"
, "message A {"
, " ;"
, " int32 x = 1;"
, " ;"
, " message Inner {"
, " int32 y = 1;"
, " };"
, "}"
, ";"
]
, it "preserves proto2 group source verbatim" $
exactPrintTest $
T.unlines
[ "syntax = \"proto2\";"
, "message M {"
, " repeated group Result = 1 {"
, " optional int32 x = 1;"
, " }"
, "}"
]
, it "preserves extensions options source verbatim" $
exactPrintTest $
T.unlines
[ "syntax = \"proto2\";"
, "message M {"
, " extensions 4 to 8 [verification = UNVERIFIED];"
, "}"
]
]
, describe
"AST inspection"
$ sequence_
[ it "allMessages finds top-level and nested" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let msgs = allMessages pf
(any (\m -> msgName m == "Person") msgs) `shouldBe` True
(any (\m -> msgName m == "PhoneNumber") msgs) `shouldBe` True
(any (\m -> msgName m == "Address") msgs) `shouldBe` True
(any (\m -> msgName m == "AddressBook") msgs) `shouldBe` True
, it "findMessage" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
(isJust (findMessage "Person" pf)) `shouldBe` True
(isNothing (findMessage "Nonexistent" pf)) `shouldBe` True
, it "allEnums" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let enums = allEnums pf
(any (\e -> enumName e == "Status") enums) `shouldBe` True
(any (\e -> enumName e == "PhoneType") enums) `shouldBe` True
, it "allServices" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let svcs = allServices pf
length svcs `shouldBe` 1
svcName (head svcs) `shouldBe` "PersonService"
, it "messageFields" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> case findMessage "Person" pf of
Nothing -> expectationFailure "Person not found"
Just msg -> do
let fields = messageFields msg
(any (\f -> fieldName f == "name") fields) `shouldBe` True
(any (\f -> fieldName f == "id") fields) `shouldBe` True
(any (\f -> fieldName f == "email") fields) `shouldBe` True
, it "nestedMessages" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> case findMessage "Person" pf of
Nothing -> expectationFailure "Person not found"
Just msg -> do
let nested = nestedMessages msg
(any (\m -> msgName m == "PhoneNumber") nested) `shouldBe` True
(any (\m -> msgName m == "Address") nested) `shouldBe` True
, it "messageOneofs" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> case findMessage "Person" pf of
Nothing -> expectationFailure "Person not found"
Just msg -> do
let oneofs = messageOneofs msg
length oneofs `shouldBe` 1
oneofName (head oneofs) `shouldBe` "contact"
, it "messageMapFields" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> case findMessage "Person" pf of
Nothing -> expectationFailure "Person not found"
Just msg -> do
let maps = messageMapFields msg
length maps `shouldBe` 1
mapFieldName (head maps) `shouldBe` "metadata"
, it "allTypeNames" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let types = allTypeNames pf
("Person" `elem` types) `shouldBe` True
("Status" `elem` types) `shouldBe` True
, it "referencedTypes" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let refs = referencedTypes pf
("PhoneNumber" `elem` refs) `shouldBe` True
("Status" `elem` refs) `shouldBe` True
, it "summarize" $ do
case parseProtoFile "<test>" complexProto of
Left e -> expectationFailure (show e)
Right pf -> do
let s = summarize pf
summSyntax s `shouldBe` Proto3
summPackage s `shouldBe` Just "example.api"
summMessageCount s > 0 `shouldBe` True
summEnumCount s > 0 `shouldBe` True
summServiceCount s `shouldBe` 1
]
]
roundtripTest :: Text -> IO ()
roundtripTest src =
case parseProtoFile "<test>" src of
Left e -> expectationFailure ("Initial parse failed: " <> show e)
Right ast1 -> do
let printed = printProtoFile ast1
case parseProtoFile "<printed>" printed of
Left e -> expectationFailure ("Re-parse failed: " <> show e <> "\n\nPrinted:\n" <> T.unpack printed)
Right ast2 -> ast1 `shouldBe` ast2
-- | Parse with spans, then assert 'exactPrint' reproduces the source byte-for-byte.
exactPrintTest :: Text -> IO ()
exactPrintTest src =
case parseProtoFileWithSpans "<test>" src of
Left e -> expectationFailure ("Parse failed: " <> show e)
Right ast -> exactPrint ast `shouldBe` src
complexProto :: Text
complexProto =
T.unlines
[ "syntax = \"proto3\";"
, "package example.api;"
, ""
, "import \"google/protobuf/timestamp.proto\";"
, "import public \"common.proto\";"
, ""
, "option java_package = \"com.example.api\";"
, "option (custom_file_opt) = true;"
, ""
, "enum Status {"
, " STATUS_UNKNOWN = 0;"
, " STATUS_ACTIVE = 1;"
, " STATUS_INACTIVE = 2;"
, "}"
, ""
, "message Person {"
, " string name = 1;"
, " int32 id = 2;"
, " string email = 3;"
, ""
, " enum PhoneType {"
, " MOBILE = 0;"
, " HOME = 1;"
, " WORK = 2;"
, " }"
, ""
, " message PhoneNumber {"
, " string number = 1;"
, " PhoneType type = 2;"
, " }"
, ""
, " message Address {"
, " string street = 1;"
, " string city = 2;"
, " }"
, ""
, " repeated PhoneNumber phones = 4;"
, " Status status = 5;"
, " map<string, string> metadata = 6;"
, ""
, " oneof contact {"
, " string phone = 7;"
, " string email_alt = 8;"
, " }"
, ""
, " reserved 10, 12 to 15;"
, " reserved \"old_field\";"
, "}"
, ""
, "message AddressBook {"
, " repeated Person people = 1;"
, "}"
, ""
, "service PersonService {"
, " rpc GetPerson (GetPersonRequest) returns (Person);"
, " rpc ListPersons (ListRequest) returns (stream Person);"
, " rpc UpdatePerson (stream Person) returns (UpdateResponse) {"
, " option deprecated = true;"
, " }"
, "}"
]