| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package filedesc_test |
| |
| import ( |
| "fmt" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| |
| detrand "google.golang.org/protobuf/internal/detrand" |
| "google.golang.org/protobuf/internal/filedesc" |
| "google.golang.org/protobuf/proto" |
| pdesc "google.golang.org/protobuf/reflect/protodesc" |
| pref "google.golang.org/protobuf/reflect/protoreflect" |
| |
| "google.golang.org/protobuf/types/descriptorpb" |
| ) |
| |
| func init() { |
| // Disable detrand to enable direct comparisons on outputs. |
| detrand.Disable() |
| } |
| |
| // TODO: Test protodesc.NewFile with imported files. |
| |
| func TestFile(t *testing.T) { |
| f1 := &descriptorpb.FileDescriptorProto{ |
| Syntax: proto.String("proto2"), |
| Name: proto.String("path/to/file.proto"), |
| Package: proto.String("test"), |
| Options: &descriptorpb.FileOptions{Deprecated: proto.Bool(true)}, |
| MessageType: []*descriptorpb.DescriptorProto{{ |
| Name: proto.String("A"), |
| Options: &descriptorpb.MessageOptions{ |
| Deprecated: proto.Bool(true), |
| }, |
| }, { |
| Name: proto.String("B"), |
| Field: []*descriptorpb.FieldDescriptorProto{{ |
| Name: proto.String("field_one"), |
| Number: proto.Int32(1), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.StringKind).Enum(), |
| DefaultValue: proto.String("hello, \"world!\"\n"), |
| OneofIndex: proto.Int32(0), |
| }, { |
| Name: proto.String("field_two"), |
| JsonName: proto.String("Field2"), |
| Number: proto.Int32(2), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.EnumKind).Enum(), |
| DefaultValue: proto.String("BAR"), |
| TypeName: proto.String(".test.E1"), |
| OneofIndex: proto.Int32(1), |
| }, { |
| Name: proto.String("field_three"), |
| Number: proto.Int32(3), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), |
| TypeName: proto.String(".test.C"), |
| OneofIndex: proto.Int32(1), |
| }, { |
| Name: proto.String("field_four"), |
| JsonName: proto.String("Field4"), |
| Number: proto.Int32(4), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), |
| TypeName: proto.String(".test.B.FieldFourEntry"), |
| }, { |
| Name: proto.String("field_five"), |
| Number: proto.Int32(5), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.Int32Kind).Enum(), |
| Options: &descriptorpb.FieldOptions{Packed: proto.Bool(true)}, |
| }, { |
| Name: proto.String("field_six"), |
| Number: proto.Int32(6), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Required).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.BytesKind).Enum(), |
| }}, |
| OneofDecl: []*descriptorpb.OneofDescriptorProto{ |
| { |
| Name: proto.String("O1"), |
| Options: &descriptorpb.OneofOptions{ |
| UninterpretedOption: []*descriptorpb.UninterpretedOption{ |
| {StringValue: []byte("option")}, |
| }, |
| }, |
| }, |
| {Name: proto.String("O2")}, |
| }, |
| ReservedName: []string{"fizz", "buzz"}, |
| ReservedRange: []*descriptorpb.DescriptorProto_ReservedRange{ |
| {Start: proto.Int32(100), End: proto.Int32(200)}, |
| {Start: proto.Int32(300), End: proto.Int32(301)}, |
| }, |
| ExtensionRange: []*descriptorpb.DescriptorProto_ExtensionRange{ |
| {Start: proto.Int32(1000), End: proto.Int32(2000)}, |
| {Start: proto.Int32(3000), End: proto.Int32(3001), Options: new(descriptorpb.ExtensionRangeOptions)}, |
| }, |
| NestedType: []*descriptorpb.DescriptorProto{{ |
| Name: proto.String("FieldFourEntry"), |
| Field: []*descriptorpb.FieldDescriptorProto{{ |
| Name: proto.String("key"), |
| Number: proto.Int32(1), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.StringKind).Enum(), |
| }, { |
| Name: proto.String("value"), |
| Number: proto.Int32(2), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), |
| TypeName: proto.String(".test.B"), |
| }}, |
| Options: &descriptorpb.MessageOptions{ |
| MapEntry: proto.Bool(true), |
| }, |
| }}, |
| }, { |
| Name: proto.String("C"), |
| NestedType: []*descriptorpb.DescriptorProto{{ |
| Name: proto.String("A"), |
| Field: []*descriptorpb.FieldDescriptorProto{{ |
| Name: proto.String("F"), |
| Number: proto.Int32(1), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Required).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.BytesKind).Enum(), |
| DefaultValue: proto.String(`dead\276\357`), |
| }}, |
| }}, |
| EnumType: []*descriptorpb.EnumDescriptorProto{{ |
| Name: proto.String("E1"), |
| Value: []*descriptorpb.EnumValueDescriptorProto{ |
| {Name: proto.String("FOO"), Number: proto.Int32(0)}, |
| {Name: proto.String("BAR"), Number: proto.Int32(1)}, |
| }, |
| }}, |
| Extension: []*descriptorpb.FieldDescriptorProto{{ |
| Name: proto.String("X"), |
| Number: proto.Int32(1000), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), |
| TypeName: proto.String(".test.C"), |
| Extendee: proto.String(".test.B"), |
| }}, |
| }}, |
| EnumType: []*descriptorpb.EnumDescriptorProto{{ |
| Name: proto.String("E1"), |
| Options: &descriptorpb.EnumOptions{Deprecated: proto.Bool(true)}, |
| Value: []*descriptorpb.EnumValueDescriptorProto{ |
| { |
| Name: proto.String("FOO"), |
| Number: proto.Int32(0), |
| Options: &descriptorpb.EnumValueOptions{Deprecated: proto.Bool(true)}, |
| }, |
| {Name: proto.String("BAR"), Number: proto.Int32(1)}, |
| }, |
| ReservedName: []string{"FIZZ", "BUZZ"}, |
| ReservedRange: []*descriptorpb.EnumDescriptorProto_EnumReservedRange{ |
| {Start: proto.Int32(10), End: proto.Int32(19)}, |
| {Start: proto.Int32(30), End: proto.Int32(30)}, |
| }, |
| }}, |
| Extension: []*descriptorpb.FieldDescriptorProto{{ |
| Name: proto.String("X"), |
| Number: proto.Int32(1000), |
| Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), |
| Type: descriptorpb.FieldDescriptorProto_Type(pref.EnumKind).Enum(), |
| Options: &descriptorpb.FieldOptions{Packed: proto.Bool(true)}, |
| TypeName: proto.String(".test.E1"), |
| Extendee: proto.String(".test.B"), |
| }}, |
| Service: []*descriptorpb.ServiceDescriptorProto{{ |
| Name: proto.String("S"), |
| Options: &descriptorpb.ServiceOptions{Deprecated: proto.Bool(true)}, |
| Method: []*descriptorpb.MethodDescriptorProto{{ |
| Name: proto.String("M"), |
| InputType: proto.String(".test.A"), |
| OutputType: proto.String(".test.C.A"), |
| ClientStreaming: proto.Bool(true), |
| ServerStreaming: proto.Bool(true), |
| Options: &descriptorpb.MethodOptions{Deprecated: proto.Bool(true)}, |
| }}, |
| }}, |
| } |
| fd1, err := pdesc.NewFile(f1, nil) |
| if err != nil { |
| t.Fatalf("protodesc.NewFile() error: %v", err) |
| } |
| |
| b, err := proto.Marshal(f1) |
| if err != nil { |
| t.Fatalf("proto.Marshal() error: %v", err) |
| } |
| fd2 := filedesc.Builder{RawDescriptor: b}.Build().File |
| |
| tests := []struct { |
| name string |
| desc pref.FileDescriptor |
| }{ |
| {"protodesc.NewFile", fd1}, |
| {"filedesc.Builder.Build", fd2}, |
| } |
| for _, tt := range tests { |
| tt := tt |
| t.Run(tt.name, func(t *testing.T) { |
| // Run sub-tests in parallel to induce potential races. |
| for i := 0; i < 2; i++ { |
| t.Run("Accessors", func(t *testing.T) { t.Parallel(); testFileAccessors(t, tt.desc) }) |
| t.Run("Format", func(t *testing.T) { t.Parallel(); testFileFormat(t, tt.desc) }) |
| } |
| }) |
| } |
| } |
| |
| func testFileAccessors(t *testing.T, fd pref.FileDescriptor) { |
| // Represent the descriptor as a map where each key is an accessor method |
| // and the value is either the wanted tail value or another accessor map. |
| type M = map[string]interface{} |
| want := M{ |
| "Parent": nil, |
| "Index": 0, |
| "Syntax": pref.Proto2, |
| "Name": pref.Name("test"), |
| "FullName": pref.FullName("test"), |
| "Path": "path/to/file.proto", |
| "Package": pref.FullName("test"), |
| "IsPlaceholder": false, |
| "Options": &descriptorpb.FileOptions{Deprecated: proto.Bool(true)}, |
| "Messages": M{ |
| "Len": 3, |
| "Get:0": M{ |
| "Parent": M{"FullName": pref.FullName("test")}, |
| "Index": 0, |
| "Syntax": pref.Proto2, |
| "Name": pref.Name("A"), |
| "FullName": pref.FullName("test.A"), |
| "IsPlaceholder": false, |
| "IsMapEntry": false, |
| "Options": &descriptorpb.MessageOptions{ |
| Deprecated: proto.Bool(true), |
| }, |
| "Oneofs": M{"Len": 0}, |
| "RequiredNumbers": M{"Len": 0}, |
| "ExtensionRanges": M{"Len": 0}, |
| "Messages": M{"Len": 0}, |
| "Enums": M{"Len": 0}, |
| "Extensions": M{"Len": 0}, |
| }, |
| "ByName:B": M{ |
| "Name": pref.Name("B"), |
| "Index": 1, |
| "Fields": M{ |
| "Len": 6, |
| "ByJSONName:field_one": nil, |
| "ByJSONName:fieldOne": M{ |
| "Name": pref.Name("field_one"), |
| "Index": 0, |
| "JSONName": "fieldOne", |
| "Default": "hello, \"world!\"\n", |
| "ContainingOneof": M{"Name": pref.Name("O1"), "IsPlaceholder": false}, |
| "ContainingMessage": M{"FullName": pref.FullName("test.B")}, |
| }, |
| "ByJSONName:fieldTwo": nil, |
| "ByJSONName:Field2": M{ |
| "Name": pref.Name("field_two"), |
| "Index": 1, |
| "HasJSONName": true, |
| "JSONName": "Field2", |
| "Default": pref.EnumNumber(1), |
| "ContainingOneof": M{"Name": pref.Name("O2"), "IsPlaceholder": false}, |
| }, |
| "ByName:fieldThree": nil, |
| "ByName:field_three": M{ |
| "IsExtension": false, |
| "IsMap": false, |
| "MapKey": nil, |
| "MapValue": nil, |
| "Message": M{"FullName": pref.FullName("test.C"), "IsPlaceholder": false}, |
| "ContainingOneof": M{"Name": pref.Name("O2"), "IsPlaceholder": false}, |
| "ContainingMessage": M{"FullName": pref.FullName("test.B")}, |
| }, |
| "ByNumber:12": nil, |
| "ByNumber:4": M{ |
| "Cardinality": pref.Repeated, |
| "IsExtension": false, |
| "IsList": false, |
| "IsMap": true, |
| "MapKey": M{"Kind": pref.StringKind}, |
| "MapValue": M{"Kind": pref.MessageKind, "Message": M{"FullName": pref.FullName("test.B")}}, |
| "Default": nil, |
| "Message": M{"FullName": pref.FullName("test.B.FieldFourEntry"), "IsPlaceholder": false}, |
| }, |
| "ByNumber:5": M{ |
| "Cardinality": pref.Repeated, |
| "Kind": pref.Int32Kind, |
| "IsPacked": true, |
| "IsList": true, |
| "IsMap": false, |
| "Default": nil, |
| }, |
| "ByNumber:6": M{ |
| "Cardinality": pref.Required, |
| "Default": []byte(nil), |
| "ContainingOneof": nil, |
| }, |
| }, |
| "Oneofs": M{ |
| "Len": 2, |
| "ByName:O0": nil, |
| "ByName:O1": M{ |
| "FullName": pref.FullName("test.B.O1"), |
| "Index": 0, |
| "Options": &descriptorpb.OneofOptions{ |
| UninterpretedOption: []*descriptorpb.UninterpretedOption{ |
| {StringValue: []byte("option")}, |
| }, |
| }, |
| "Fields": M{ |
| "Len": 1, |
| "Get:0": M{"FullName": pref.FullName("test.B.field_one")}, |
| }, |
| }, |
| "Get:1": M{ |
| "FullName": pref.FullName("test.B.O2"), |
| "Index": 1, |
| "Fields": M{ |
| "Len": 2, |
| "ByName:field_two": M{"Name": pref.Name("field_two")}, |
| "Get:1": M{"Name": pref.Name("field_three")}, |
| }, |
| }, |
| }, |
| "ReservedNames": M{ |
| "Len": 2, |
| "Get:0": pref.Name("fizz"), |
| "Has:buzz": true, |
| "Has:noexist": false, |
| }, |
| "ReservedRanges": M{ |
| "Len": 2, |
| "Get:0": [2]pref.FieldNumber{100, 200}, |
| "Has:99": false, |
| "Has:100": true, |
| "Has:150": true, |
| "Has:199": true, |
| "Has:200": false, |
| "Has:300": true, |
| "Has:301": false, |
| }, |
| "RequiredNumbers": M{ |
| "Len": 1, |
| "Get:0": pref.FieldNumber(6), |
| "Has:1": false, |
| "Has:6": true, |
| }, |
| "ExtensionRanges": M{ |
| "Len": 2, |
| "Get:0": [2]pref.FieldNumber{1000, 2000}, |
| "Has:999": false, |
| "Has:1000": true, |
| "Has:1500": true, |
| "Has:1999": true, |
| "Has:2000": false, |
| "Has:3000": true, |
| "Has:3001": false, |
| }, |
| "ExtensionRangeOptions:0": (*descriptorpb.ExtensionRangeOptions)(nil), |
| "ExtensionRangeOptions:1": new(descriptorpb.ExtensionRangeOptions), |
| "Messages": M{ |
| "Get:0": M{ |
| "Fields": M{ |
| "Len": 2, |
| "ByNumber:1": M{ |
| "Parent": M{"FullName": pref.FullName("test.B.FieldFourEntry")}, |
| "Index": 0, |
| "Name": pref.Name("key"), |
| "FullName": pref.FullName("test.B.FieldFourEntry.key"), |
| "Number": pref.FieldNumber(1), |
| "Cardinality": pref.Optional, |
| "Kind": pref.StringKind, |
| "Options": (*descriptorpb.FieldOptions)(nil), |
| "HasJSONName": false, |
| "JSONName": "key", |
| "IsPacked": false, |
| "IsList": false, |
| "IsMap": false, |
| "IsExtension": false, |
| "IsWeak": false, |
| "Default": "", |
| "ContainingOneof": nil, |
| "ContainingMessage": M{"FullName": pref.FullName("test.B.FieldFourEntry")}, |
| "Message": nil, |
| "Enum": nil, |
| }, |
| "ByNumber:2": M{ |
| "Parent": M{"FullName": pref.FullName("test.B.FieldFourEntry")}, |
| "Index": 1, |
| "Name": pref.Name("value"), |
| "FullName": pref.FullName("test.B.FieldFourEntry.value"), |
| "Number": pref.FieldNumber(2), |
| "Cardinality": pref.Optional, |
| "Kind": pref.MessageKind, |
| "JSONName": "value", |
| "IsPacked": false, |
| "IsList": false, |
| "IsMap": false, |
| "IsExtension": false, |
| "IsWeak": false, |
| "Default": nil, |
| "ContainingOneof": nil, |
| "ContainingMessage": M{"FullName": pref.FullName("test.B.FieldFourEntry")}, |
| "Message": M{"FullName": pref.FullName("test.B"), "IsPlaceholder": false}, |
| "Enum": nil, |
| }, |
| "ByNumber:3": nil, |
| }, |
| }, |
| }, |
| }, |
| "Get:2": M{ |
| "Name": pref.Name("C"), |
| "Index": 2, |
| "Messages": M{ |
| "Len": 1, |
| "Get:0": M{"FullName": pref.FullName("test.C.A")}, |
| }, |
| "Enums": M{ |
| "Len": 1, |
| "Get:0": M{"FullName": pref.FullName("test.C.E1")}, |
| }, |
| "Extensions": M{ |
| "Len": 1, |
| "Get:0": M{"FullName": pref.FullName("test.C.X")}, |
| }, |
| }, |
| }, |
| "Enums": M{ |
| "Len": 1, |
| "Get:0": M{ |
| "Name": pref.Name("E1"), |
| "Options": &descriptorpb.EnumOptions{Deprecated: proto.Bool(true)}, |
| "Values": M{ |
| "Len": 2, |
| "ByName:Foo": nil, |
| "ByName:FOO": M{ |
| "FullName": pref.FullName("test.FOO"), |
| "Options": &descriptorpb.EnumValueOptions{Deprecated: proto.Bool(true)}, |
| }, |
| "ByNumber:2": nil, |
| "ByNumber:1": M{"FullName": pref.FullName("test.BAR")}, |
| }, |
| "ReservedNames": M{ |
| "Len": 2, |
| "Get:0": pref.Name("FIZZ"), |
| "Has:BUZZ": true, |
| "Has:NOEXIST": false, |
| }, |
| "ReservedRanges": M{ |
| "Len": 2, |
| "Get:0": [2]pref.EnumNumber{10, 19}, |
| "Has:9": false, |
| "Has:10": true, |
| "Has:15": true, |
| "Has:19": true, |
| "Has:20": false, |
| "Has:30": true, |
| "Has:31": false, |
| }, |
| }, |
| }, |
| "Extensions": M{ |
| "Len": 1, |
| "ByName:X": M{ |
| "Name": pref.Name("X"), |
| "Number": pref.FieldNumber(1000), |
| "Cardinality": pref.Repeated, |
| "Kind": pref.EnumKind, |
| "IsExtension": true, |
| "IsPacked": true, |
| "IsList": true, |
| "IsMap": false, |
| "MapKey": nil, |
| "MapValue": nil, |
| "ContainingOneof": nil, |
| "ContainingMessage": M{"FullName": pref.FullName("test.B"), "IsPlaceholder": false}, |
| "Enum": M{"FullName": pref.FullName("test.E1"), "IsPlaceholder": false}, |
| "Options": &descriptorpb.FieldOptions{Packed: proto.Bool(true)}, |
| }, |
| }, |
| "Services": M{ |
| "Len": 1, |
| "ByName:s": nil, |
| "ByName:S": M{ |
| "Parent": M{"FullName": pref.FullName("test")}, |
| "Name": pref.Name("S"), |
| "FullName": pref.FullName("test.S"), |
| "Options": &descriptorpb.ServiceOptions{Deprecated: proto.Bool(true)}, |
| "Methods": M{ |
| "Len": 1, |
| "Get:0": M{ |
| "Parent": M{"FullName": pref.FullName("test.S")}, |
| "Name": pref.Name("M"), |
| "FullName": pref.FullName("test.S.M"), |
| "Input": M{"FullName": pref.FullName("test.A"), "IsPlaceholder": false}, |
| "Output": M{"FullName": pref.FullName("test.C.A"), "IsPlaceholder": false}, |
| "IsStreamingClient": true, |
| "IsStreamingServer": true, |
| "Options": &descriptorpb.MethodOptions{Deprecated: proto.Bool(true)}, |
| }, |
| }, |
| }, |
| }, |
| } |
| checkAccessors(t, "", reflect.ValueOf(fd), want) |
| } |
| func checkAccessors(t *testing.T, p string, rv reflect.Value, want map[string]interface{}) { |
| p0 := p |
| defer func() { |
| if ex := recover(); ex != nil { |
| t.Errorf("panic at %v: %v", p, ex) |
| } |
| }() |
| |
| if rv.Interface() == nil { |
| t.Errorf("%v is nil, want non-nil", p) |
| return |
| } |
| for s, v := range want { |
| // Call the accessor method. |
| p = p0 + "." + s |
| var rets []reflect.Value |
| if i := strings.IndexByte(s, ':'); i >= 0 { |
| // Accessor method takes in a single argument, which is encoded |
| // after the accessor name, separated by a ':' delimiter. |
| fnc := rv.MethodByName(s[:i]) |
| arg := reflect.New(fnc.Type().In(0)).Elem() |
| s = s[i+len(":"):] |
| switch arg.Kind() { |
| case reflect.String: |
| arg.SetString(s) |
| case reflect.Int32, reflect.Int: |
| n, _ := strconv.ParseInt(s, 0, 64) |
| arg.SetInt(n) |
| } |
| rets = fnc.Call([]reflect.Value{arg}) |
| } else { |
| rets = rv.MethodByName(s).Call(nil) |
| } |
| |
| // Check that (val, ok) pattern is internally consistent. |
| if len(rets) == 2 { |
| if rets[0].IsNil() && rets[1].Bool() { |
| t.Errorf("%v = (nil, true), want (nil, false)", p) |
| } |
| if !rets[0].IsNil() && !rets[1].Bool() { |
| t.Errorf("%v = (non-nil, false), want (non-nil, true)", p) |
| } |
| } |
| |
| // Check that the accessor output matches. |
| if want, ok := v.(map[string]interface{}); ok { |
| checkAccessors(t, p, rets[0], want) |
| continue |
| } |
| |
| got := rets[0].Interface() |
| if pv, ok := got.(pref.Value); ok { |
| got = pv.Interface() |
| } |
| |
| // Compare with proto.Equal if possible. |
| gotMsg, gotMsgOK := got.(proto.Message) |
| wantMsg, wantMsgOK := v.(proto.Message) |
| if gotMsgOK && wantMsgOK { |
| gotNil := reflect.ValueOf(gotMsg).IsNil() |
| wantNil := reflect.ValueOf(wantMsg).IsNil() |
| switch { |
| case !gotNil && wantNil: |
| t.Errorf("%v = non-nil, want nil", p) |
| case gotNil && !wantNil: |
| t.Errorf("%v = nil, want non-nil", p) |
| case !proto.Equal(gotMsg, wantMsg): |
| t.Errorf("%v = %v, want %v", p, gotMsg, wantMsg) |
| } |
| continue |
| } |
| |
| if want := v; !reflect.DeepEqual(got, want) { |
| t.Errorf("%v = %T(%v), want %T(%v)", p, got, got, want, want) |
| } |
| } |
| } |
| |
| func testFileFormat(t *testing.T, fd pref.FileDescriptor) { |
| const want = `FileDescriptor{ |
| Syntax: proto2 |
| Path: "path/to/file.proto" |
| Package: test |
| Messages: [{ |
| Name: A |
| }, { |
| Name: B |
| Fields: [{ |
| Name: field_one |
| Number: 1 |
| Cardinality: optional |
| Kind: string |
| JSONName: "fieldOne" |
| HasDefault: true |
| Default: "hello, \"world!\"\n" |
| Oneof: O1 |
| }, { |
| Name: field_two |
| Number: 2 |
| Cardinality: optional |
| Kind: enum |
| HasJSONName: true |
| JSONName: "Field2" |
| HasDefault: true |
| Default: 1 |
| Oneof: O2 |
| Enum: test.E1 |
| }, { |
| Name: field_three |
| Number: 3 |
| Cardinality: optional |
| Kind: message |
| JSONName: "fieldThree" |
| Oneof: O2 |
| Message: test.C |
| }, { |
| Name: field_four |
| Number: 4 |
| Cardinality: repeated |
| Kind: message |
| HasJSONName: true |
| JSONName: "Field4" |
| IsMap: true |
| MapKey: string |
| MapValue: test.B |
| }, { |
| Name: field_five |
| Number: 5 |
| Cardinality: repeated |
| Kind: int32 |
| JSONName: "fieldFive" |
| IsPacked: true |
| IsList: true |
| }, { |
| Name: field_six |
| Number: 6 |
| Cardinality: required |
| Kind: bytes |
| JSONName: "fieldSix" |
| }] |
| Oneofs: [{ |
| Name: O1 |
| Fields: [field_one] |
| }, { |
| Name: O2 |
| Fields: [field_two, field_three] |
| }] |
| ReservedNames: [fizz, buzz] |
| ReservedRanges: [100:200, 300] |
| RequiredNumbers: [6] |
| ExtensionRanges: [1000:2000, 3000] |
| Messages: [{ |
| Name: FieldFourEntry |
| IsMapEntry: true |
| Fields: [{ |
| Name: key |
| Number: 1 |
| Cardinality: optional |
| Kind: string |
| JSONName: "key" |
| }, { |
| Name: value |
| Number: 2 |
| Cardinality: optional |
| Kind: message |
| JSONName: "value" |
| Message: test.B |
| }] |
| }] |
| }, { |
| Name: C |
| Messages: [{ |
| Name: A |
| Fields: [{ |
| Name: F |
| Number: 1 |
| Cardinality: required |
| Kind: bytes |
| JSONName: "F" |
| HasDefault: true |
| Default: "dead\xbe\xef" |
| }] |
| RequiredNumbers: [1] |
| }] |
| Enums: [{ |
| Name: E1 |
| Values: [ |
| {Name: FOO} |
| {Name: BAR, Number: 1} |
| ] |
| }] |
| Extensions: [{ |
| Name: X |
| Number: 1000 |
| Cardinality: repeated |
| Kind: message |
| JSONName: "X" |
| IsExtension: true |
| IsList: true |
| Extendee: test.B |
| Message: test.C |
| }] |
| }] |
| Enums: [{ |
| Name: E1 |
| Values: [ |
| {Name: FOO} |
| {Name: BAR, Number: 1} |
| ] |
| ReservedNames: [FIZZ, BUZZ] |
| ReservedRanges: [10:20, 30] |
| }] |
| Extensions: [{ |
| Name: X |
| Number: 1000 |
| Cardinality: repeated |
| Kind: enum |
| JSONName: "X" |
| IsPacked: true |
| IsExtension: true |
| IsList: true |
| Extendee: test.B |
| Enum: test.E1 |
| }] |
| Services: [{ |
| Name: S |
| Methods: [{ |
| Name: M |
| Input: test.A |
| Output: test.C.A |
| IsStreamingClient: true |
| IsStreamingServer: true |
| }] |
| }] |
| }` |
| tests := []struct{ fmt, want string }{{"%v", compactMultiFormat(want)}, {"%+v", want}} |
| for _, tt := range tests { |
| got := fmt.Sprintf(tt.fmt, fd) |
| if diff := cmp.Diff(got, tt.want); diff != "" { |
| t.Errorf("fmt.Sprintf(%q, fd) mismatch (-got +want):\n%s", tt.fmt, diff) |
| } |
| } |
| } |
| |
| // compactMultiFormat returns the single line form of a multi line output. |
| func compactMultiFormat(s string) string { |
| var b []byte |
| for _, s := range strings.Split(s, "\n") { |
| s = strings.TrimSpace(s) |
| s = regexp.MustCompile(": +").ReplaceAllString(s, ": ") |
| prevWord := len(b) > 0 && b[len(b)-1] != '[' && b[len(b)-1] != '{' |
| nextWord := len(s) > 0 && s[0] != ']' && s[0] != '}' |
| if prevWord && nextWord { |
| b = append(b, ", "...) |
| } |
| b = append(b, s...) |
| } |
| return string(b) |
| } |