packages feed

wireform-proto-0.1.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.Tasty
import Test.Tasty.HUnit


printInspectTests :: TestTree
printInspectTests =
  testGroup
    "Print & Inspect"
    [ testGroup
        "Exact printing"
        [ testCase "roundtrip: parse -> print -> parse yields same AST" $ do
            let original = complexProto
            case parseProtoFile "<test>" original of
              Left e -> assertFailure (show e)
              Right ast1 -> do
                let printed = printProtoFile ast1
                case parseProtoFile "<printed>" printed of
                  Left e -> assertFailure ("Re-parse failed: " <> show e <> "\n\nPrinted:\n" <> T.unpack printed)
                  Right ast2 -> ast1 @?= ast2
        , testCase "syntax declaration" $ do
            let ast = ProtoFile Proto3 Nothing [] [] [] Nothing
            T.isInfixOf "syntax = \"proto3\";" (printProtoFile ast) @?= True
        , testCase "syntax proto2" $ do
            let ast = ProtoFile Proto2 Nothing [] [] [] Nothing
            T.isInfixOf "syntax = \"proto2\";" (printProtoFile ast) @?= True
        , testCase "package declaration" $ do
            let ast = ProtoFile Proto3 (Just "my.package") [] [] [] Nothing
            T.isInfixOf "package my.package;" (printProtoFile ast) @?= True
        , testCase "import" $ do
            let ast = ProtoFile Proto3 Nothing [ImportDef () Nothing "other.proto"] [] [] Nothing
            T.isInfixOf "import \"other.proto\";" (printProtoFile ast) @?= True
        , testCase "public import" $ do
            let ast = ProtoFile Proto3 Nothing [ImportDef () (Just ImportPublic) "other.proto"] [] [] Nothing
            T.isInfixOf "import public \"other.proto\";" (printProtoFile ast) @?= True
        , testCase "simple message roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Foo {"
                    , "  string name = 1;"
                    , "  int32 value = 2;"
                    , "}"
                    ]
            roundtripTest src
        , testCase "enum roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "enum Status {"
                    , "  UNKNOWN = 0;"
                    , "  ACTIVE = 1;"
                    , "}"
                    ]
            roundtripTest src
        , testCase "service roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "service Greeter {"
                    , "  rpc SayHello (HelloReq) returns (HelloReply);"
                    , "}"
                    ]
            roundtripTest src
        , testCase "map field roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Foo {"
                    , "  map<string, int32> labels = 1;"
                    , "}"
                    ]
            roundtripTest src
        , testCase "oneof roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Foo {"
                    , "  oneof val {"
                    , "    string text = 1;"
                    , "    int32 number = 2;"
                    , "  }"
                    , "}"
                    ]
            roundtripTest src
        , testCase "reserved roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Foo {"
                    , "  reserved 2, 10 to 20;"
                    , "  reserved \"old_field\";"
                    , "}"
                    ]
            roundtripTest src
        , testCase "field options roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Foo {"
                    , "  string name = 1 [deprecated = true];"
                    , "}"
                    ]
            roundtripTest src
        , testCase "option with extension name roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "option (my_custom_opt) = true;"
                    , "message Foo {"
                    , "  string name = 1;"
                    , "}"
                    ]
            roundtripTest src
        , testCase "nested message roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "message Outer {"
                    , "  message Inner {"
                    , "    int32 x = 1;"
                    , "  }"
                    , "  Inner inner = 1;"
                    , "}"
                    ]
            roundtripTest src
        , testCase "streaming rpc roundtrip" $ do
            let src =
                  T.unlines
                    [ "syntax = \"proto3\";"
                    , "service Chat {"
                    , "  rpc Stream (stream Msg) returns (stream Msg);"
                    , "}"
                    ]
            roundtripTest src
        ]
    , testGroup
        "AST inspection"
        [ testCase "allMessages finds top-level and nested" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let msgs = allMessages pf
                assertBool "Should find Person" (any (\m -> msgName m == "Person") msgs)
                assertBool "Should find PhoneNumber" (any (\m -> msgName m == "PhoneNumber") msgs)
                assertBool "Should find Address" (any (\m -> msgName m == "Address") msgs)
                assertBool "Should find AddressBook" (any (\m -> msgName m == "AddressBook") msgs)
        , testCase "findMessage" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                assertBool "Find Person" (isJust (findMessage "Person" pf))
                assertBool "No Nonexistent" (isNothing (findMessage "Nonexistent" pf))
        , testCase "allEnums" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let enums = allEnums pf
                assertBool "Should find Status" (any (\e -> enumName e == "Status") enums)
                assertBool "Should find PhoneType" (any (\e -> enumName e == "PhoneType") enums)
        , testCase "allServices" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let svcs = allServices pf
                length svcs @?= 1
                svcName (head svcs) @?= "PersonService"
        , testCase "messageFields" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> case findMessage "Person" pf of
                Nothing -> assertFailure "Person not found"
                Just msg -> do
                  let fields = messageFields msg
                  assertBool "Has name field" (any (\f -> fieldName f == "name") fields)
                  assertBool "Has id field" (any (\f -> fieldName f == "id") fields)
                  assertBool "Has email field" (any (\f -> fieldName f == "email") fields)
        , testCase "nestedMessages" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> case findMessage "Person" pf of
                Nothing -> assertFailure "Person not found"
                Just msg -> do
                  let nested = nestedMessages msg
                  assertBool "Has PhoneNumber" (any (\m -> msgName m == "PhoneNumber") nested)
                  assertBool "Has Address" (any (\m -> msgName m == "Address") nested)
        , testCase "messageOneofs" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> case findMessage "Person" pf of
                Nothing -> assertFailure "Person not found"
                Just msg -> do
                  let oneofs = messageOneofs msg
                  length oneofs @?= 1
                  oneofName (head oneofs) @?= "contact"
        , testCase "messageMapFields" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> case findMessage "Person" pf of
                Nothing -> assertFailure "Person not found"
                Just msg -> do
                  let maps = messageMapFields msg
                  length maps @?= 1
                  mapFieldName (head maps) @?= "metadata"
        , testCase "allTypeNames" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let types = allTypeNames pf
                assertBool "Has Person" ("Person" `elem` types)
                assertBool "Has Status" ("Status" `elem` types)
        , testCase "referencedTypes" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let refs = referencedTypes pf
                assertBool "References PhoneNumber" ("PhoneNumber" `elem` refs)
                assertBool "References Status" ("Status" `elem` refs)
        , testCase "summarize" $ do
            case parseProtoFile "<test>" complexProto of
              Left e -> assertFailure (show e)
              Right pf -> do
                let s = summarize pf
                summSyntax s @?= Proto3
                summPackage s @?= Just "example.api"
                summMessageCount s > 0 @?= True
                summEnumCount s > 0 @?= True
                summServiceCount s @?= 1
        ]
    ]


roundtripTest :: Text -> IO ()
roundtripTest src =
  case parseProtoFile "<test>" src of
    Left e -> assertFailure ("Initial parse failed: " <> show e)
    Right ast1 -> do
      let printed = printProtoFile ast1
      case parseProtoFile "<printed>" printed of
        Left e -> assertFailure ("Re-parse failed: " <> show e <> "\n\nPrinted:\n" <> T.unpack printed)
        Right ast2 -> ast1 @?= ast2


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;"
    , "  }"
    , "}"
    ]