// Copyright 2016 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 proto_test import ( "bytes" "errors" "math" "strings" "sync" "testing" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" pb2 "github.com/golang/protobuf/internal/testprotos/proto2_proto" pb3 "github.com/golang/protobuf/internal/testprotos/proto3_proto" anypb "github.com/golang/protobuf/ptypes/any" ) var ( expandedMarshaler = proto.TextMarshaler{ExpandAny: true} expandedCompactMarshaler = proto.TextMarshaler{Compact: true, ExpandAny: true} ) // anyEqual reports whether two messages which may be google.protobuf.Any or may // contain google.protobuf.Any fields are equal. We can't use proto.Equal for // comparison, because semantically equivalent messages may be marshaled to // binary in different tag order. Instead, trust that TextMarshaler with // ExpandAny option works and compare the text marshaling results. func anyEqual(got, want proto.Message) bool { // if messages are proto.Equal, no need to marshal. if proto.Equal(got, want) { return true } g := expandedMarshaler.Text(got) w := expandedMarshaler.Text(want) return g == w } type golden struct { m proto.Message t, c string } var goldenMessages = makeGolden() func makeGolden() []golden { nested := &pb3.Nested{Bunny: "Monty"} nb, err := proto.Marshal(nested) if err != nil { panic(err) } m1 := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(nested), Value: nb}, } m2 := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: "http://[::1]/type.googleapis.com/" + proto.MessageName(nested), Value: nb}, } m3 := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: `type.googleapis.com/"/` + proto.MessageName(nested), Value: nb}, } m4 := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: "type.googleapis.com/a/path/" + proto.MessageName(nested), Value: nb}, } m5 := &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(nested), Value: nb} any1 := &pb2.MyMessage{Count: proto.Int32(47), Name: proto.String("David")} proto.SetExtension(any1, pb2.E_Ext_More, &pb2.Ext{Data: proto.String("foo")}) proto.SetExtension(any1, pb2.E_Ext_Text, proto.String("bar")) any1b, err := proto.Marshal(any1) if err != nil { panic(err) } any2 := &pb2.MyMessage{Count: proto.Int32(42), Bikeshed: pb2.MyMessage_GREEN.Enum(), RepBytes: [][]byte{[]byte("roboto")}} proto.SetExtension(any2, pb2.E_Ext_More, &pb2.Ext{Data: proto.String("baz")}) any2b, err := proto.Marshal(any2) if err != nil { panic(err) } m6 := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(any1), Value: any1b}, ManyThings: []*anypb.Any{ &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(any2), Value: any2b}, &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(any1), Value: any1b}, }, } const ( m1Golden = ` name: "David" result_count: 47 anything: < [type.googleapis.com/proto3_test.Nested]: < bunny: "Monty" > > ` m2Golden = ` name: "David" result_count: 47 anything: < ["http://[::1]/type.googleapis.com/proto3_test.Nested"]: < bunny: "Monty" > > ` m3Golden = ` name: "David" result_count: 47 anything: < ["type.googleapis.com/\"/proto3_test.Nested"]: < bunny: "Monty" > > ` m4Golden = ` name: "David" result_count: 47 anything: < [type.googleapis.com/a/path/proto3_test.Nested]: < bunny: "Monty" > > ` m5Golden = ` [type.googleapis.com/proto3_test.Nested]: < bunny: "Monty" > ` m6Golden = ` name: "David" result_count: 47 anything: < [type.googleapis.com/proto2_test.MyMessage]: < count: 47 name: "David" [proto2_test.Ext.more]: < data: "foo" > [proto2_test.Ext.text]: "bar" > > many_things: < [type.googleapis.com/proto2_test.MyMessage]: < count: 42 bikeshed: GREEN rep_bytes: "roboto" [proto2_test.Ext.more]: < data: "baz" > > > many_things: < [type.googleapis.com/proto2_test.MyMessage]: < count: 47 name: "David" [proto2_test.Ext.more]: < data: "foo" > [proto2_test.Ext.text]: "bar" > > ` ) return []golden{ {m1, strings.TrimSpace(m1Golden) + "\n", strings.TrimSpace(compact(m1Golden)) + " "}, {m2, strings.TrimSpace(m2Golden) + "\n", strings.TrimSpace(compact(m2Golden)) + " "}, {m3, strings.TrimSpace(m3Golden) + "\n", strings.TrimSpace(compact(m3Golden)) + " "}, {m4, strings.TrimSpace(m4Golden) + "\n", strings.TrimSpace(compact(m4Golden)) + " "}, {m5, strings.TrimSpace(m5Golden) + "\n", strings.TrimSpace(compact(m5Golden)) + " "}, {m6, strings.TrimSpace(m6Golden) + "\n", strings.TrimSpace(compact(m6Golden)) + " "}, } } func TestMarshalGolden(t *testing.T) { for _, tt := range goldenMessages { t.Run("", func(t *testing.T) { if got, want := expandedMarshaler.Text(tt.m), tt.t; got != want { t.Errorf("message %v: got:\n%s\nwant:\n%s", tt.m, got, want) } if got, want := expandedCompactMarshaler.Text(tt.m), tt.c; got != want { t.Errorf("message %v: got:\n`%s`\nwant:\n`%s`", tt.m, got, want) } }) } } func TestUnmarshalGolden(t *testing.T) { for _, tt := range goldenMessages { t.Run("", func(t *testing.T) { want := tt.m got := proto.Clone(tt.m) got.Reset() if err := proto.UnmarshalText(tt.t, got); err != nil { t.Errorf("failed to unmarshal\n%s\nerror: %v", tt.t, err) } if !anyEqual(got, want) { t.Errorf("message:\n%s\ngot:\n%s\nwant:\n%s", tt.t, got, want) } got.Reset() if err := proto.UnmarshalText(tt.c, got); err != nil { t.Errorf("failed to unmarshal\n%s\nerror: %v", tt.c, err) } if !anyEqual(got, want) { t.Errorf("message:\n%s\ngot:\n%s\nwant:\n%s", tt.c, got, want) } }) } } func TestMarshalUnknownAny(t *testing.T) { m := &pb3.Message{ Anything: &anypb.Any{ TypeUrl: "foo", Value: []byte("bar"), }, } want := `anything: < type_url: "foo" value: "bar" > ` got := expandedMarshaler.Text(m) if got != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestAmbiguousAny(t *testing.T) { pb := &anypb.Any{} err := proto.UnmarshalText(` type_url: "ttt/proto3_test.Nested" value: "\n\x05Monty" `, pb) if err != nil { t.Errorf("unexpected proto.UnmarshalText error: %v", err) } } func TestUnmarshalOverwriteAny(t *testing.T) { pb := &anypb.Any{} err := proto.UnmarshalText(` [type.googleapis.com/a/path/proto3_test.Nested]: < bunny: "Monty" > [type.googleapis.com/a/path/proto3_test.Nested]: < bunny: "Rabbit of Caerbannog" > `, pb) want := `line 7: Any message unpacked multiple times, or "type_url" already set` if err.Error() != want { t.Errorf("incorrect error:\ngot: %v\nwant: %v", err.Error(), want) } } func TestUnmarshalAnyMixAndMatch(t *testing.T) { pb := &anypb.Any{} err := proto.UnmarshalText(` value: "\n\x05Monty" [type.googleapis.com/a/path/proto3_test.Nested]: < bunny: "Rabbit of Caerbannog" > `, pb) want := `line 5: Any message unpacked multiple times, or "value" already set` if err.Error() != want { t.Errorf("incorrect error:\ngot: %v\nwant: %v", err.Error(), want) } } // textMessage implements the methods that allow it to marshal and unmarshal // itself as text. type textMessage struct { } func (*textMessage) MarshalText() ([]byte, error) { return []byte("custom"), nil } func (*textMessage) UnmarshalText(bytes []byte) error { if string(bytes) != "custom" { return errors.New("expected 'custom'") } return nil } func (*textMessage) Reset() {} func (*textMessage) String() string { return "" } func (*textMessage) ProtoMessage() {} func newTestMessage() *pb2.MyMessage { msg := &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("Dave"), Quote: proto.String(`"I didn't want to go."`), Pet: []string{"bunny", "kitty", "horsey"}, Inner: &pb2.InnerMessage{ Host: proto.String("footrest.syd"), Port: proto.Int32(7001), Connected: proto.Bool(true), }, Others: []*pb2.OtherMessage{ { Key: proto.Int64(0xdeadbeef), Value: []byte{1, 65, 7, 12}, }, { Weight: proto.Float32(6.022), Inner: &pb2.InnerMessage{ Host: proto.String("lesha.mtv"), Port: proto.Int32(8002), }, }, }, Bikeshed: pb2.MyMessage_BLUE.Enum(), Somegroup: &pb2.MyMessage_SomeGroup{ GroupField: proto.Int32(8), }, // One normally wouldn't do this. // This is an undeclared tag 13, as a varint (wire type 0) with value 4. XXX_unrecognized: []byte{13<<3 | 0, 4}, } ext := &pb2.Ext{ Data: proto.String("Big gobs for big rats"), } if err := proto.SetExtension(msg, pb2.E_Ext_More, ext); err != nil { panic(err) } greetings := []string{"adg", "easy", "cow"} if err := proto.SetExtension(msg, pb2.E_Greeting, greetings); err != nil { panic(err) } // Add an unknown extension. We marshal a pb2.Ext, and fake the ID. b, err := proto.Marshal(&pb2.Ext{Data: proto.String("3G skiing")}) if err != nil { panic(err) } b = append(proto.EncodeVarint(201<<3|proto.WireBytes), b...) proto.SetRawExtension(msg, 201, b) // Extensions can be plain fields, too, so let's test that. b = append(proto.EncodeVarint(202<<3|proto.WireVarint), 19) proto.SetRawExtension(msg, 202, b) return msg } const text = `count: 42 name: "Dave" quote: "\"I didn't want to go.\"" pet: "bunny" pet: "kitty" pet: "horsey" inner: < host: "footrest.syd" port: 7001 connected: true > others: < key: 3735928559 value: "\001A\007\014" > others: < weight: 6.022 inner: < host: "lesha.mtv" port: 8002 > > bikeshed: BLUE SomeGroup { group_field: 8 } /* 18 unknown bytes */ 13: 4 201: "\t3G skiing" 202: 19 [proto2_test.Ext.more]: < data: "Big gobs for big rats" > [proto2_test.greeting]: "adg" [proto2_test.greeting]: "easy" [proto2_test.greeting]: "cow" ` func TestMarshalText(t *testing.T) { buf := new(bytes.Buffer) if err := proto.MarshalText(buf, newTestMessage()); err != nil { t.Fatalf("proto.MarshalText: %v", err) } got := buf.String() if diff := cmp.Diff(text, got); got != text { t.Errorf("diff (-want +got):\n%v\n\ngot:\n%v\n\nwant:\n%v", diff, got, text) } } func TestMarshalTextCustomMessage(t *testing.T) { buf := new(bytes.Buffer) if err := proto.MarshalText(buf, &textMessage{}); err != nil { t.Fatalf("proto.MarshalText: %v", err) } got := buf.String() if got != "custom" { t.Errorf("got:\n%v\n\nwant:\n%v", got, "custom") } } func TestMarshalTextNil(t *testing.T) { want := "" tests := []proto.Message{nil, (*pb2.MyMessage)(nil)} for i, test := range tests { buf := new(bytes.Buffer) if err := proto.MarshalText(buf, test); err != nil { t.Fatal(err) } if got := buf.String(); got != want { t.Errorf("%d: got %q want %q", i, got, want) } } } func TestMarshalTextUnknownEnum(t *testing.T) { // The Color enum only specifies values 0-2. m := &pb2.MyMessage{Bikeshed: pb2.MyMessage_Color(3).Enum()} got := m.String() const want = `bikeshed:3 ` if got != want { t.Errorf("\n got %q\nwant %q", got, want) } } func TestTextOneof(t *testing.T) { tests := []struct { m proto.Message want string }{ // zero message {&pb2.Communique{}, ``}, // scalar field {&pb2.Communique{Union: &pb2.Communique_Number{4}}, `number:4`}, // message field {&pb2.Communique{Union: &pb2.Communique_Msg{ &pb2.Strings{StringField: proto.String("why hello!")}, }}, `msg:`}, // bad oneof (should not panic) {&pb2.Communique{Union: &pb2.Communique_Msg{nil}}, `msg:<>`}, } for _, test := range tests { got := strings.TrimSpace(test.m.String()) if got != test.want { t.Errorf("got:\n%s\n\nwant:\n%s", got, test.want) } } } func compact(src string) string { // s/[ \n]+/ /g; s/ $//; dst := make([]byte, len(src)) space, comment := false, false j := 0 for i := 0; i < len(src); i++ { if strings.HasPrefix(src[i:], "/*") { comment = true i++ continue } if comment && strings.HasPrefix(src[i:], "*/") { comment = false i++ continue } if comment { continue } c := src[i] if c == ' ' || c == '\n' { space = true continue } if j > 0 && (dst[j-1] == ':' || dst[j-1] == '<' || dst[j-1] == '{') { space = false } if c == '{' { space = false } if space { dst[j] = ' ' j++ space = false } dst[j] = c j++ } if space { dst[j] = ' ' j++ } return string(dst[0:j]) } func TestCompactText(t *testing.T) { got := proto.CompactTextString(newTestMessage()) if got != compact(text) { t.Errorf("got:\n%v\n\nwant:\n%v", got, compact(text)) } } func TestStringEscaping(t *testing.T) { testCases := []struct { in *pb2.Strings out string }{ { // Test data from C++ test (TextFormatTest.StringEscape). // Single divergence: we don't escape apostrophes. &pb2.Strings{StringField: proto.String("\"A string with ' characters \n and \r newlines and \t tabs and \001 slashes \\ and multiple spaces")}, "string_field: \"\\\"A string with ' characters \\n and \\r newlines and \\t tabs and \\001 slashes \\\\ and multiple spaces\"\n", }, { // Test data from the same C++ test. &pb2.Strings{StringField: proto.String("\350\260\267\346\255\214")}, "string_field: \"\\350\\260\\267\\346\\255\\214\"\n", }, { // Some UTF-8. &pb2.Strings{StringField: proto.String("\x00\x01\xff\x81")}, `string_field: "\000\001\377\201"` + "\n", }, } for _, tc := range testCases { t.Run("", func(t *testing.T) { var buf bytes.Buffer if err := proto.MarshalText(&buf, tc.in); err != nil { t.Fatalf("proto.MarsalText error: %v", err) } got := buf.String() if got != tc.out { t.Fatalf("want:\n%s\n\nwant:\n%s", got, tc.out) } // Check round-trip. pb := new(pb2.Strings) if err := proto.UnmarshalText(got, pb); err != nil { t.Fatalf("proto.UnmarshalText error: %v", err) } if !proto.Equal(pb, tc.in) { t.Fatalf("proto.Equal mismatch:\ngot:\n%v\n\nwant:\n%v", pb, tc.in) } }) } } // A limitedWriter accepts some output before it fails. // This is a proxy for something like a nearly-full or imminently-failing disk, // or a network connection that is about to die. type limitedWriter struct { b bytes.Buffer limit int } var outOfSpace = errors.New("proto: insufficient space") func (w *limitedWriter) Write(p []byte) (n int, err error) { var avail = w.limit - w.b.Len() if avail <= 0 { return 0, outOfSpace } if len(p) <= avail { return w.b.Write(p) } n, _ = w.b.Write(p[:avail]) return n, outOfSpace } func TestMarshalTextFailing(t *testing.T) { // Try lots of different sizes to exercise more error code-paths. for lim := 0; lim < len(text); lim++ { buf := new(limitedWriter) buf.limit = lim err := proto.MarshalText(buf, newTestMessage()) // We expect a certain error, but also some partial results in the buffer. if err != outOfSpace { t.Errorf("error mismatch: got %v, want %v", err, outOfSpace) } got := buf.b.String() want := text[:buf.limit] if got != want { t.Errorf("text mismatch:\n\ngot:\n%v\n\nwant:\n%v", got, want) } } } func TestFloats(t *testing.T) { tests := []struct { f float64 want string }{ {0, "0"}, {4.7, "4.7"}, {math.Inf(1), "inf"}, {math.Inf(-1), "-inf"}, {math.NaN(), "nan"}, } for _, test := range tests { msg := &pb2.FloatingPoint{F: &test.f} got := strings.TrimSpace(msg.String()) want := `f:` + test.want if got != want { t.Errorf("f=%f: got %q, want %q", test.f, got, want) } } } func TestRepeatedNilText(t *testing.T) { m := &pb2.MessageList{ Message: []*pb2.MessageList_Message{ nil, &pb2.MessageList_Message{ Name: proto.String("Horse"), }, nil, }, } want := `Message { } Message { name: "Horse" } Message { } ` if got := proto.MarshalTextString(m); got != want { t.Errorf("got:\n%s\n\nwant:\n%s", got, want) } } func TestProto3Text(t *testing.T) { tests := []struct { m proto.Message want string }{ // zero message {&pb3.Message{}, ``}, // zero message except for an empty byte slice {&pb3.Message{Data: []byte{}}, ``}, // trivial case {&pb3.Message{Name: "Rob", HeightInCm: 175}, `name:"Rob" height_in_cm:175`}, // empty map {&pb2.MessageWithMap{}, ``}, // non-empty map; map format is the same as a repeated struct, // and they are sorted by key (numerically for numeric keys). { &pb2.MessageWithMap{NameMapping: map[int32]string{ -1: "Negatory", 7: "Lucky", 1234: "Feist", 6345789: "Otis", }}, `name_mapping: ` + `name_mapping: ` + `name_mapping: ` + `name_mapping:`, }, // map with nil value; not well-defined, but we shouldn't crash { &pb2.MessageWithMap{MsgMapping: map[int64]*pb2.FloatingPoint{7: nil}}, `msg_mapping: >`, }, } for _, test := range tests { got := strings.TrimSpace(test.m.String()) if got != test.want { t.Errorf("got:\n%s\n\nwant:\n%s", got, test.want) } } } func TestRacyMarshal(t *testing.T) { // This test should be run with the race detector. any := &pb2.MyMessage{Count: proto.Int32(47), Name: proto.String("David")} proto.SetExtension(any, pb2.E_Ext_Text, proto.String("bar")) b, err := proto.Marshal(any) if err != nil { panic(err) } m := &pb3.Message{ Name: "David", ResultCount: 47, Anything: &anypb.Any{TypeUrl: "type.googleapis.com/" + proto.MessageName(any), Value: b}, } wantText := proto.MarshalTextString(m) wantBytes, err := proto.Marshal(m) if err != nil { t.Fatalf("proto.Marshal error: %v", err) } var wg sync.WaitGroup defer wg.Wait() wg.Add(20) for i := 0; i < 10; i++ { go func() { defer wg.Done() got := proto.MarshalTextString(m) if got != wantText { t.Errorf("proto.MarshalTextString = %q, want %q", got, wantText) } }() go func() { defer wg.Done() got, err := proto.Marshal(m) if !bytes.Equal(got, wantBytes) || err != nil { t.Errorf("proto.Marshal = (%x, %v), want (%x, nil)", got, err, wantBytes) } }() } } type UnmarshalTextTest struct { in string err string // if "", no error expected out *pb2.MyMessage } func buildExtStructTest(text string) UnmarshalTextTest { msg := &pb2.MyMessage{ Count: proto.Int32(42), } proto.SetExtension(msg, pb2.E_Ext_More, &pb2.Ext{ Data: proto.String("Hello, world!"), }) return UnmarshalTextTest{in: text, out: msg} } func buildExtDataTest(text string) UnmarshalTextTest { msg := &pb2.MyMessage{ Count: proto.Int32(42), } proto.SetExtension(msg, pb2.E_Ext_Text, proto.String("Hello, world!")) proto.SetExtension(msg, pb2.E_Ext_Number, proto.Int32(1729)) return UnmarshalTextTest{in: text, out: msg} } func buildExtRepStringTest(text string) UnmarshalTextTest { msg := &pb2.MyMessage{ Count: proto.Int32(42), } if err := proto.SetExtension(msg, pb2.E_Greeting, []string{"bula", "hola"}); err != nil { panic(err) } return UnmarshalTextTest{in: text, out: msg} } var unmarshalTextTests = []UnmarshalTextTest{ // Basic { in: " count:42\n name:\"Dave\" ", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("Dave"), }, }, // Empty quoted string { in: `count:42 name:""`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String(""), }, }, // Quoted string concatenation with double quotes { in: `count:42 name: "My name is "` + "\n" + `"elsewhere"`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("My name is elsewhere"), }, }, // Quoted string concatenation with single quotes { in: "count:42 name: 'My name is '\n'elsewhere'", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("My name is elsewhere"), }, }, // Quoted string concatenations with mixed quotes { in: "count:42 name: 'My name is '\n\"elsewhere\"", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("My name is elsewhere"), }, }, { in: "count:42 name: \"My name is \"\n'elsewhere'", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("My name is elsewhere"), }, }, // Quoted string with escaped apostrophe { in: `count:42 name: "HOLIDAY - New Year\'s Day"`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("HOLIDAY - New Year's Day"), }, }, // Quoted string with single quote { in: `count:42 name: 'Roger "The Ramster" Ramjet'`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String(`Roger "The Ramster" Ramjet`), }, }, // Quoted string with all the accepted special characters from the C++ test { in: `count:42 name: ` + "\"\\\"A string with \\' characters \\n and \\r newlines and \\t tabs and \\001 slashes \\\\ and multiple spaces\"", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("\"A string with ' characters \n and \r newlines and \t tabs and \001 slashes \\ and multiple spaces"), }, }, // Quoted string with quoted backslash { in: `count:42 name: "\\'xyz"`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String(`\'xyz`), }, }, // Quoted string with UTF-8 bytes. { in: "count:42 name: '\303\277\302\201\x00\xAB\xCD\xEF'", out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("\303\277\302\201\x00\xAB\xCD\xEF"), }, }, // Quoted string with unicode escapes. { in: `count: 42 name: "\u0047\U00000047\uffff\U0010ffff"`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("GG\uffff\U0010ffff"), }, }, // Bad quoted string { in: `inner: < host: "\0" >` + "\n", err: `line 1.15: invalid quoted string "\0": \0 requires 2 following digits`, }, // Bad \u escape { in: `count: 42 name: "\u000"`, err: `line 1.16: invalid quoted string "\u000": \u requires 4 following digits`, }, // Bad \U escape { in: `count: 42 name: "\U0000000"`, err: `line 1.16: invalid quoted string "\U0000000": \U requires 8 following digits`, }, // Bad \U escape { in: `count: 42 name: "\xxx"`, err: `line 1.16: invalid quoted string "\xxx": \xxx contains non-hexadecimal digits`, }, // Number too large for int64 { in: "count: 1 others { key: 123456789012345678901 }", err: "line 1.23: invalid int64: 123456789012345678901", }, // Number too large for int32 { in: "count: 1234567890123", err: "line 1.7: invalid int32: 1234567890123", }, // Number in hexadecimal { in: "count: 0x2beef", out: &pb2.MyMessage{ Count: proto.Int32(0x2beef), }, }, // Number in octal { in: "count: 024601", out: &pb2.MyMessage{ Count: proto.Int32(024601), }, }, // Floating point number with "f" suffix { in: "count: 4 others:< weight: 17.0f >", out: &pb2.MyMessage{ Count: proto.Int32(4), Others: []*pb2.OtherMessage{ { Weight: proto.Float32(17), }, }, }, }, // Floating point positive infinity { in: "count: 4 bigfloat: inf", out: &pb2.MyMessage{ Count: proto.Int32(4), Bigfloat: proto.Float64(math.Inf(1)), }, }, // Floating point negative infinity { in: "count: 4 bigfloat: -inf", out: &pb2.MyMessage{ Count: proto.Int32(4), Bigfloat: proto.Float64(math.Inf(-1)), }, }, // Number too large for float32 { in: "others:< weight: 12345678901234567890123456789012345678901234567890 >", err: "line 1.17: invalid float: 12345678901234567890123456789012345678901234567890", }, // Number posing as a quoted string { in: `inner: < host: 12 >` + "\n", err: `line 1.15: invalid string: 12`, }, // Quoted string posing as int32 { in: `count: "12"`, err: `line 1.7: invalid int32: "12"`, }, // Quoted string posing a float32 { in: `others:< weight: "17.4" >`, err: `line 1.17: invalid float: "17.4"`, }, // unclosed bracket doesn't cause infinite loop { in: `[`, err: `line 1.0: unclosed type_url or extension name`, }, // Enum { in: `count:42 bikeshed: BLUE`, out: &pb2.MyMessage{ Count: proto.Int32(42), Bikeshed: pb2.MyMessage_BLUE.Enum(), }, }, // Repeated field { in: `count:42 pet: "horsey" pet:"bunny"`, out: &pb2.MyMessage{ Count: proto.Int32(42), Pet: []string{"horsey", "bunny"}, }, }, // Repeated field with list notation { in: `count:42 pet: ["horsey", "bunny"]`, out: &pb2.MyMessage{ Count: proto.Int32(42), Pet: []string{"horsey", "bunny"}, }, }, // Repeated message with/without colon and <>/{} { in: `count:42 others:{} others{} others:<> others:{}`, out: &pb2.MyMessage{ Count: proto.Int32(42), Others: []*pb2.OtherMessage{ {}, {}, {}, {}, }, }, }, // Missing colon for inner message { in: `count:42 inner < host: "cauchy.syd" >`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("cauchy.syd"), }, }, }, // Missing colon for string field { in: `name "Dave"`, err: `line 1.5: expected ':', found "\"Dave\""`, }, // Missing colon for int32 field { in: `count 42`, err: `line 1.6: expected ':', found "42"`, }, // Missing required field { in: `name: "Pawel"`, err: `required field proto2_test.MyMessage.count not set`, out: &pb2.MyMessage{ Name: proto.String("Pawel"), }, }, // Missing required field in a required submessage { in: `count: 42 we_must_go_deeper < leo_finally_won_an_oscar <> >`, err: `required field proto2_test.InnerMessage.host not set`, out: &pb2.MyMessage{ Count: proto.Int32(42), WeMustGoDeeper: &pb2.RequiredInnerMessage{LeoFinallyWonAnOscar: &pb2.InnerMessage{}}, }, }, // Repeated non-repeated field { in: `name: "Rob" name: "Russ"`, err: `line 1.12: non-repeated field "name" was repeated`, }, // Group { in: `count: 17 SomeGroup { group_field: 12 }`, out: &pb2.MyMessage{ Count: proto.Int32(17), Somegroup: &pb2.MyMessage_SomeGroup{ GroupField: proto.Int32(12), }, }, }, // Semicolon between fields { in: `count:3;name:"Calvin"`, out: &pb2.MyMessage{ Count: proto.Int32(3), Name: proto.String("Calvin"), }, }, // Comma between fields { in: `count:4,name:"Ezekiel"`, out: &pb2.MyMessage{ Count: proto.Int32(4), Name: proto.String("Ezekiel"), }, }, // Boolean false { in: `count:42 inner { host: "example.com" connected: false }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(false), }, }, }, // Boolean true { in: `count:42 inner { host: "example.com" connected: true }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(true), }, }, }, // Boolean 0 { in: `count:42 inner { host: "example.com" connected: 0 }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(false), }, }, }, // Boolean 1 { in: `count:42 inner { host: "example.com" connected: 1 }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(true), }, }, }, // Boolean f { in: `count:42 inner { host: "example.com" connected: f }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(false), }, }, }, // Boolean t { in: `count:42 inner { host: "example.com" connected: t }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(true), }, }, }, // Boolean False { in: `count:42 inner { host: "example.com" connected: False }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(false), }, }, }, // Boolean True { in: `count:42 inner { host: "example.com" connected: True }`, out: &pb2.MyMessage{ Count: proto.Int32(42), Inner: &pb2.InnerMessage{ Host: proto.String("example.com"), Connected: proto.Bool(true), }, }, }, // Extension buildExtStructTest(`count: 42 [proto2_test.Ext.more]:`), buildExtStructTest(`count: 42 [proto2_test.Ext.more] {data:"Hello, world!"}`), buildExtDataTest(`count: 42 [proto2_test.Ext.text]:"Hello, world!" [proto2_test.Ext.number]:1729`), buildExtRepStringTest(`count: 42 [proto2_test.greeting]:"bula" [proto2_test.greeting]:"hola"`), { in: `[proto2_test.complex]:<>`, err: `line 1.20: extension field "proto2_test.complex" does not extend message "proto2_test.MyMessage"`, }, // Big all-in-one { in: "count:42 # Meaning\n" + `name:"Dave" ` + `quote:"\"I didn't want to go.\"" ` + `pet:"bunny" ` + `pet:"kitty" ` + `pet:"horsey" ` + `inner:<` + ` host:"footrest.syd" ` + ` port:7001 ` + ` connected:true ` + `> ` + `others:<` + ` key:3735928559 ` + ` value:"\x01A\a\f" ` + `> ` + `others:<` + " weight:58.9 # Atomic weight of Co\n" + ` inner:<` + ` host:"lesha.mtv" ` + ` port:8002 ` + ` >` + `>`, out: &pb2.MyMessage{ Count: proto.Int32(42), Name: proto.String("Dave"), Quote: proto.String(`"I didn't want to go."`), Pet: []string{"bunny", "kitty", "horsey"}, Inner: &pb2.InnerMessage{ Host: proto.String("footrest.syd"), Port: proto.Int32(7001), Connected: proto.Bool(true), }, Others: []*pb2.OtherMessage{ { Key: proto.Int64(3735928559), Value: []byte{0x1, 'A', '\a', '\f'}, }, { Weight: proto.Float32(58.9), Inner: &pb2.InnerMessage{ Host: proto.String("lesha.mtv"), Port: proto.Int32(8002), }, }, }, }, }, } func TestUnmarshalText(t *testing.T) { for _, test := range unmarshalTextTests { t.Run("", func(t *testing.T) { pb := new(pb2.MyMessage) err := proto.UnmarshalText(test.in, pb) if test.err == "" { // We don't expect failure. if err != nil { t.Errorf("proto.UnmarshalText error: %v", err) } else if !proto.Equal(pb, test.out) { t.Errorf("proto.Equal mismatch:\ngot: %v\nwant: %v", pb, test.out) } } else { // We do expect failure. if err == nil { t.Errorf("proto.UnmarshalText: got nil error, want %v", test.err) } else if !strings.Contains(err.Error(), test.err) { t.Errorf("proto.UnmarshalText error mismatch:\ngot: %v\nwant: %v", err.Error(), test.err) } else if _, ok := err.(*proto.RequiredNotSetError); ok && test.out != nil && !proto.Equal(pb, test.out) { t.Errorf("proto.Equal mismatch:\ngot %v\nwant: %v", pb, test.out) } } }) } } func TestUnmarshalTextCustomMessage(t *testing.T) { msg := &textMessage{} if err := proto.UnmarshalText("custom", msg); err != nil { t.Errorf("proto.UnmarshalText error: %v", err) } if err := proto.UnmarshalText("not custom", msg); err == nil { t.Errorf("proto.UnmarshalText: got nil error, want non-nil") } } // Regression test; this caused a panic. func TestRepeatedEnum(t *testing.T) { pb := new(pb2.RepeatedEnum) if err := proto.UnmarshalText("color: RED", pb); err != nil { t.Fatal(err) } exp := &pb2.RepeatedEnum{ Color: []pb2.RepeatedEnum_Color{pb2.RepeatedEnum_RED}, } if !proto.Equal(pb, exp) { t.Errorf("proto.Equal mismatch:\ngot: %v\nwant %v", pb, exp) } } func TestProto3TextParsing(t *testing.T) { m := new(pb3.Message) const in = `name: "Wallace" true_scotsman: true` want := &pb3.Message{ Name: "Wallace", TrueScotsman: true, } if err := proto.UnmarshalText(in, m); err != nil { t.Fatal(err) } if !proto.Equal(m, want) { t.Errorf("proto.Equal mismatch:\ngot: %v\nwant %v", m, want) } } func TestMapParsing(t *testing.T) { m := new(pb2.MessageWithMap) const in = `name_mapping: name_mapping:` + `msg_mapping:,>` + // separating commas are okay `msg_mapping>` + // no colon after "value" `msg_mapping:>` + // omitted key `byte_mapping:` + `byte_mapping:<>` // omitted key and value want := &pb2.MessageWithMap{ NameMapping: map[int32]string{ 1: "Beatles", 1234: "Feist", }, MsgMapping: map[int64]*pb2.FloatingPoint{ -4: {F: proto.Float64(2.0)}, -2: {F: proto.Float64(4.0)}, 0: {F: proto.Float64(5.0)}, }, ByteMapping: map[bool][]byte{ false: nil, true: []byte("so be it"), }, } if err := proto.UnmarshalText(in, m); err != nil { t.Fatal(err) } if !proto.Equal(m, want) { t.Errorf("proto.Equal mismatch:\ngot: %v\nwant %v", m, want) } } func TestOneofParsing(t *testing.T) { const in = `name:"Shrek"` m := new(pb2.Communique) want := &pb2.Communique{Union: &pb2.Communique_Name{"Shrek"}} if err := proto.UnmarshalText(in, m); err != nil { t.Fatal(err) } if !proto.Equal(m, want) { t.Errorf("\n got %v\nwant %v", m, want) } const inOverwrite = `name:"Shrek" number:42` m = new(pb2.Communique) testErr := "line 1.13: field 'number' would overwrite already parsed oneof 'union'" if err := proto.UnmarshalText(inOverwrite, m); err == nil { t.Errorf("proto.UnmarshalText: got nil error, want %v", testErr) } else if err.Error() != testErr { t.Errorf("error mismatch:\ngot: %v\nwant: %v", err.Error(), testErr) } }