encoding/textpb: initial implementation of textproto marshaling

This initial implementation covers marshaling Message without use
of extensions, Any expansion, weak yet.

Change-Id: Ic787939c1d2a4e70e40c3a1654c6e7073052b7d3
Reviewed-on: https://go-review.googlesource.com/c/151677
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/textpb/encode_test.go b/encoding/textpb/encode_test.go
new file mode 100644
index 0000000..28beaf4
--- /dev/null
+++ b/encoding/textpb/encode_test.go
@@ -0,0 +1,933 @@
+// 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 textpb_test
+
+import (
+	"math"
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/v2/encoding/textpb"
+	"github.com/golang/protobuf/v2/encoding/textpb/testprotos/pb2"
+	"github.com/golang/protobuf/v2/encoding/textpb/testprotos/pb3"
+	"github.com/golang/protobuf/v2/internal/detrand"
+	"github.com/golang/protobuf/v2/internal/impl"
+	"github.com/golang/protobuf/v2/internal/scalar"
+	"github.com/golang/protobuf/v2/proto"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+
+	anypb "github.com/golang/protobuf/ptypes/any"
+	durpb "github.com/golang/protobuf/ptypes/duration"
+	emptypb "github.com/golang/protobuf/ptypes/empty"
+	stpb "github.com/golang/protobuf/ptypes/struct"
+	tspb "github.com/golang/protobuf/ptypes/timestamp"
+	wpb "github.com/golang/protobuf/ptypes/wrappers"
+)
+
+func init() {
+	// Disable detrand to enable direct comparisons on outputs.
+	detrand.Disable()
+}
+
+func M(m interface{}) proto.Message {
+	return impl.MessageOf(m).Interface()
+}
+
+// splitLines is a cmpopts.Option for comparing strings with line breaks.
+var splitLines = cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
+	return strings.Split(s, "\n")
+})
+
+func TestMarshal(t *testing.T) {
+	tests := []struct {
+		desc    string
+		input   proto.Message
+		want    string
+		wantErr bool
+	}{{
+		desc: "nil message",
+		want: "\n",
+	}, {
+		desc:  "proto2 optional scalar fields not set",
+		input: M(&pb2.Scalars{}),
+		want:  "\n",
+	}, {
+		desc:  "proto3 scalar fields not set",
+		input: M(&pb3.Scalars{}),
+		want:  "\n",
+	}, {
+		desc: "proto2 optional scalar fields set to zero values",
+		input: M(&pb2.Scalars{
+			OptBool:     scalar.Bool(false),
+			OptInt32:    scalar.Int32(0),
+			OptInt64:    scalar.Int64(0),
+			OptUint32:   scalar.Uint32(0),
+			OptUint64:   scalar.Uint64(0),
+			OptSint32:   scalar.Int32(0),
+			OptSint64:   scalar.Int64(0),
+			OptFixed32:  scalar.Uint32(0),
+			OptFixed64:  scalar.Uint64(0),
+			OptSfixed32: scalar.Int32(0),
+			OptSfixed64: scalar.Int64(0),
+			OptFloat:    scalar.Float32(0),
+			OptDouble:   scalar.Float64(0),
+			OptBytes:    []byte{},
+			OptString:   scalar.String(""),
+		}),
+		want: `opt_bool: false
+opt_int32: 0
+opt_int64: 0
+opt_uint32: 0
+opt_uint64: 0
+opt_sint32: 0
+opt_sint64: 0
+opt_fixed32: 0
+opt_fixed64: 0
+opt_sfixed32: 0
+opt_sfixed64: 0
+opt_float: 0
+opt_double: 0
+opt_bytes: ""
+opt_string: ""
+`,
+	}, {
+		desc: "proto3 scalar fields set to zero values",
+		input: M(&pb3.Scalars{
+			SBool:     false,
+			SInt32:    0,
+			SInt64:    0,
+			SUint32:   0,
+			SUint64:   0,
+			SSint32:   0,
+			SSint64:   0,
+			SFixed32:  0,
+			SFixed64:  0,
+			SSfixed32: 0,
+			SSfixed64: 0,
+			SFloat:    0,
+			SDouble:   0,
+			SBytes:    []byte{},
+			SString:   "",
+		}),
+		want: "\n",
+	}, {
+		desc: "proto2 optional scalar fields set to some values",
+		input: M(&pb2.Scalars{
+			OptBool:     scalar.Bool(true),
+			OptInt32:    scalar.Int32(0xff),
+			OptInt64:    scalar.Int64(0xdeadbeef),
+			OptUint32:   scalar.Uint32(47),
+			OptUint64:   scalar.Uint64(0xdeadbeef),
+			OptSint32:   scalar.Int32(-1001),
+			OptSint64:   scalar.Int64(-0xffff),
+			OptFixed64:  scalar.Uint64(64),
+			OptSfixed32: scalar.Int32(-32),
+			// TODO: Update encoder to output same decimals.
+			OptFloat:  scalar.Float32(1.02),
+			OptDouble: scalar.Float64(1.23e100),
+			// TODO: Update encoder to not output UTF8 for bytes.
+			OptBytes:  []byte("\xe8\xb0\xb7\xe6\xad\x8c"),
+			OptString: scalar.String("谷歌"),
+		}),
+		want: `opt_bool: true
+opt_int32: 255
+opt_int64: 3735928559
+opt_uint32: 47
+opt_uint64: 3735928559
+opt_sint32: -1001
+opt_sint64: -65535
+opt_fixed64: 64
+opt_sfixed32: -32
+opt_float: 1.0199999809265137
+opt_double: 1.23e+100
+opt_bytes: "谷歌"
+opt_string: "谷歌"
+`,
+	}, {
+		desc:  "proto3 enum empty message",
+		input: M(&pb3.Enums{}),
+		want:  "\n",
+	}, {
+		desc: "proto3 enum",
+		input: M(&pb3.Enums{
+			SEnum:         pb3.Enum_ONE,
+			RptEnum:       []pb3.Enum{pb3.Enum_ONE, 10, 0, 21, -1},
+			SNestedEnum:   pb3.Enums_DIEZ,
+			RptNestedEnum: []pb3.Enums_NestedEnum{21, pb3.Enums_CERO, -7, 10},
+		}),
+		want: `s_enum: ONE
+rpt_enum: ONE
+rpt_enum: TEN
+rpt_enum: ZERO
+rpt_enum: 21
+rpt_enum: -1
+s_nested_enum: DIEZ
+rpt_nested_enum: 21
+rpt_nested_enum: CERO
+rpt_nested_enum: -7
+rpt_nested_enum: DIEZ
+`,
+	}, {
+		desc: "float32 nan",
+		input: M(&pb3.Scalars{
+			SFloat: float32(math.NaN()),
+		}),
+		want: "s_float: nan\n",
+	}, {
+		desc: "float32 positive infinity",
+		input: M(&pb3.Scalars{
+			SFloat: float32(math.Inf(1)),
+		}),
+		want: "s_float: inf\n",
+	}, {
+		desc: "float32 negative infinity",
+		input: M(&pb3.Scalars{
+			SFloat: float32(math.Inf(-1)),
+		}),
+		want: "s_float: -inf\n",
+	}, {
+		desc: "float64 nan",
+		input: M(&pb3.Scalars{
+			SDouble: math.NaN(),
+		}),
+		want: "s_double: nan\n",
+	}, {
+		desc: "float64 positive infinity",
+		input: M(&pb3.Scalars{
+			SDouble: math.Inf(1),
+		}),
+		want: "s_double: inf\n",
+	}, {
+		desc: "float64 negative infinity",
+		input: M(&pb3.Scalars{
+			SDouble: math.Inf(-1),
+		}),
+		want: "s_double: -inf\n",
+	}, {
+		desc: "proto2 bytes set to empty string",
+		input: M(&pb2.Scalars{
+			OptBytes: []byte(""),
+		}),
+		want: "opt_bytes: \"\"\n",
+	}, {
+		desc: "proto3 bytes set to empty string",
+		input: M(&pb3.Scalars{
+			SBytes: []byte(""),
+		}),
+		want: "\n",
+	}, {
+		desc:  "proto2 repeated not set",
+		input: M(&pb2.Repeats{}),
+		want:  "\n",
+	}, {
+		desc: "proto2 repeated set to empty slices",
+		input: M(&pb2.Repeats{
+			RptBool:   []bool{},
+			RptInt32:  []int32{},
+			RptInt64:  []int64{},
+			RptUint32: []uint32{},
+			RptUint64: []uint64{},
+			RptFloat:  []float32{},
+			RptDouble: []float64{},
+			RptBytes:  [][]byte{},
+		}),
+		want: "\n",
+	}, {
+		desc: "proto2 repeated set to some values",
+		input: M(&pb2.Repeats{
+			RptBool:   []bool{true, false, true, true},
+			RptInt32:  []int32{1, 6, 0, 0},
+			RptInt64:  []int64{-64, 47},
+			RptUint32: []uint32{0xff, 0xffff},
+			RptUint64: []uint64{0xdeadbeef},
+			// TODO: add float32 examples.
+			RptDouble: []float64{math.NaN(), math.Inf(1), math.Inf(-1), 1.23e-308},
+			RptString: []string{"hello", "世界"},
+			RptBytes: [][]byte{
+				[]byte("hello"),
+				[]byte("\xe4\xb8\x96\xe7\x95\x8c"),
+			},
+		}),
+		want: `rpt_bool: true
+rpt_bool: false
+rpt_bool: true
+rpt_bool: true
+rpt_int32: 1
+rpt_int32: 6
+rpt_int32: 0
+rpt_int32: 0
+rpt_int64: -64
+rpt_int64: 47
+rpt_uint32: 255
+rpt_uint32: 65535
+rpt_uint64: 3735928559
+rpt_double: nan
+rpt_double: inf
+rpt_double: -inf
+rpt_double: 1.23e-308
+rpt_string: "hello"
+rpt_string: "世界"
+rpt_bytes: "hello"
+rpt_bytes: "世界"
+`,
+	}, {
+		desc:  "proto2 enum fields not set",
+		input: M(&pb2.Enums{}),
+		want:  "\n",
+	}, {
+		desc: "proto2 enum fields",
+		input: M(&pb2.Enums{
+			OptEnum:       pb2.Enum_FIRST.Enum(),
+			RptEnum:       []pb2.Enum{pb2.Enum_FIRST, 2, pb2.Enum_TENTH, 42},
+			OptNestedEnum: pb2.Enums_UNO.Enum(),
+			RptNestedEnum: []pb2.Enums_NestedEnum{2, 47, 10},
+		}),
+		want: `opt_enum: FIRST
+rpt_enum: FIRST
+rpt_enum: SECOND
+rpt_enum: TENTH
+rpt_enum: 42
+opt_nested_enum: UNO
+rpt_nested_enum: DOS
+rpt_nested_enum: 47
+rpt_nested_enum: DIEZ
+`,
+	}, {
+		desc: "proto3 enum fields set to zero value",
+		input: M(&pb3.Enums{
+			SEnum:         pb3.Enum_ZERO,
+			RptEnum:       []pb3.Enum{},
+			SNestedEnum:   pb3.Enums_CERO,
+			RptNestedEnum: []pb3.Enums_NestedEnum{},
+		}),
+		want: "\n",
+	}, {
+		desc: "proto3 enum fields",
+		input: M(&pb3.Enums{
+			SEnum:         pb3.Enum_TWO,
+			RptEnum:       []pb3.Enum{1, 0, 0},
+			SNestedEnum:   pb3.Enums_DOS,
+			RptNestedEnum: []pb3.Enums_NestedEnum{101, pb3.Enums_DIEZ, 10},
+		}),
+		want: `s_enum: TWO
+rpt_enum: ONE
+rpt_enum: ZERO
+rpt_enum: ZERO
+s_nested_enum: DOS
+rpt_nested_enum: 101
+rpt_nested_enum: DIEZ
+rpt_nested_enum: DIEZ
+`,
+	}, {
+		desc:  "proto2 nested message not set",
+		input: M(&pb2.Nests{}),
+		want:  "\n",
+	}, {
+		desc: "proto2 nested message set to empty",
+		input: M(&pb2.Nests{
+			OptNested: &pb2.Nested{},
+			Optgroup:  &pb2.Nests_OptGroup{},
+			RptNested: []*pb2.Nested{},
+			Rptgroup:  []*pb2.Nests_RptGroup{},
+		}),
+		want: `opt_nested: {}
+optgroup: {}
+`,
+	}, {
+		desc: "proto2 nested messages",
+		input: M(&pb2.Nests{
+			OptNested: &pb2.Nested{
+				OptString: scalar.String("nested message"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("another nested message"),
+				},
+			},
+			RptNested: []*pb2.Nested{
+				{
+					OptString: scalar.String("repeat nested one"),
+				},
+				{
+					OptString: scalar.String("repeat nested two"),
+					OptNested: &pb2.Nested{
+						OptString: scalar.String("inside repeat nested two"),
+					},
+				},
+				{},
+			},
+		}),
+		want: `opt_nested: {
+  opt_string: "nested message"
+  opt_nested: {
+    opt_string: "another nested message"
+  }
+}
+rpt_nested: {
+  opt_string: "repeat nested one"
+}
+rpt_nested: {
+  opt_string: "repeat nested two"
+  opt_nested: {
+    opt_string: "inside repeat nested two"
+  }
+}
+rpt_nested: {}
+`,
+	}, {
+		desc: "proto2 group fields",
+		input: M(&pb2.Nests{
+			Optgroup: &pb2.Nests_OptGroup{
+				OptBool:   scalar.Bool(true),
+				OptString: scalar.String("inside a group"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("nested message inside a group"),
+				},
+				Optnestedgroup: &pb2.Nests_OptGroup_OptNestedGroup{
+					OptEnum: pb2.Enum_TENTH.Enum(),
+				},
+			},
+			Rptgroup: []*pb2.Nests_RptGroup{
+				{
+					RptBool: []bool{true, false},
+				},
+				{},
+			},
+		}),
+		want: `optgroup: {
+  opt_bool: true
+  opt_string: "inside a group"
+  opt_nested: {
+    opt_string: "nested message inside a group"
+  }
+  optnestedgroup: {
+    opt_enum: TENTH
+  }
+}
+rptgroup: {
+  rpt_bool: true
+  rpt_bool: false
+}
+rptgroup: {}
+`,
+	}, {
+		desc:  "proto3 nested message not set",
+		input: M(&pb3.Nests{}),
+		want:  "\n",
+	}, {
+		desc: "proto3 nested message",
+		input: M(&pb3.Nests{
+			SNested: &pb3.Nested{
+				SString: "nested message",
+				SNested: &pb3.Nested{
+					SString: "another nested message",
+				},
+			},
+			RptNested: []*pb3.Nested{
+				{
+					SString: "repeated nested one",
+					SNested: &pb3.Nested{
+						SString: "inside repeated nested one",
+					},
+				},
+				{
+					SString: "repeated nested two",
+				},
+				{},
+			},
+		}),
+		want: `s_nested: {
+  s_string: "nested message"
+  s_nested: {
+    s_string: "another nested message"
+  }
+}
+rpt_nested: {
+  s_string: "repeated nested one"
+  s_nested: {
+    s_string: "inside repeated nested one"
+  }
+}
+rpt_nested: {
+  s_string: "repeated nested two"
+}
+rpt_nested: {}
+`,
+	}, {
+		desc:    "proto2 required fields not set",
+		input:   M(&pb2.Requireds{}),
+		want:    "\n",
+		wantErr: true,
+	}, {
+		desc: "proto2 required fields partially set",
+		input: M(&pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqFixed32:  scalar.Uint32(47),
+			ReqSfixed64: scalar.Int64(0xbeefcafe),
+			ReqDouble:   scalar.Float64(math.NaN()),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_FIRST.Enum(),
+		}),
+		want: `req_bool: false
+req_fixed32: 47
+req_sfixed64: 3203386110
+req_double: nan
+req_string: "hello"
+req_enum: FIRST
+`,
+		wantErr: true,
+	}, {
+		desc: "proto2 required fields all set",
+		input: M(&pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqFixed32:  scalar.Uint32(0),
+			ReqFixed64:  scalar.Uint64(0),
+			ReqSfixed32: scalar.Int32(0),
+			ReqSfixed64: scalar.Int64(0),
+			ReqFloat:    scalar.Float32(0),
+			ReqDouble:   scalar.Float64(0),
+			ReqString:   scalar.String(""),
+			ReqEnum:     pb2.Enum_UNKNOWN.Enum(),
+			ReqBytes:    []byte{},
+			ReqNested:   &pb2.Nested{},
+		}),
+		want: `req_bool: false
+req_fixed32: 0
+req_fixed64: 0
+req_sfixed32: 0
+req_sfixed64: 0
+req_float: 0
+req_double: 0
+req_string: ""
+req_bytes: ""
+req_enum: UNKNOWN
+req_nested: {}
+`,
+	}, {
+		desc:  "oneof fields",
+		input: M(&pb2.Oneofs{}),
+		want:  "\n",
+	}, {
+		desc: "oneof field set to empty string",
+		input: M(&pb2.Oneofs{
+			Union: &pb2.Oneofs_Str{},
+		}),
+		want: "str: \"\"\n",
+	}, {
+		desc: "oneof field set to string",
+		input: M(&pb2.Oneofs{
+			Union: &pb2.Oneofs_Str{
+				Str: "hello",
+			},
+		}),
+		want: "str: \"hello\"\n",
+	}, {
+		desc: "oneof field set to empty message",
+		input: M(&pb2.Oneofs{
+			Union: &pb2.Oneofs_Msg{
+				Msg: &pb2.Nested{},
+			},
+		}),
+		want: "msg: {}\n",
+	}, {
+		desc: "oneof field set to message",
+		input: M(&pb2.Oneofs{
+			Union: &pb2.Oneofs_Msg{
+				Msg: &pb2.Nested{
+					OptString: scalar.String("nested message"),
+				},
+			},
+		}),
+		want: `msg: {
+  opt_string: "nested message"
+}
+`,
+	}, {
+		desc:  "map fields empty",
+		input: M(&pb2.Maps{}),
+		want:  "\n",
+	}, {
+		desc: "map fields set to empty maps",
+		input: M(&pb2.Maps{
+			Int32ToStr:     map[int32]string{},
+			Sfixed64ToBool: map[int64]bool{},
+			BoolToUint32:   map[bool]uint32{},
+			Uint64ToEnum:   map[uint64]pb2.Enum{},
+			StrToNested:    map[string]*pb2.Nested{},
+			StrToOneofs:    map[string]*pb2.Oneofs{},
+		}),
+		want: "\n",
+	}, {
+		desc: "map fields 1",
+		input: M(&pb2.Maps{
+			Int32ToStr: map[int32]string{
+				-101: "-101",
+				0xff: "0xff",
+				0:    "zero",
+			},
+			Sfixed64ToBool: map[int64]bool{
+				0xcafe: true,
+				0:      false,
+			},
+			BoolToUint32: map[bool]uint32{
+				true:  42,
+				false: 101,
+			},
+		}),
+		want: `int32_to_str: {
+  key: -101
+  value: "-101"
+}
+int32_to_str: {
+  key: 0
+  value: "zero"
+}
+int32_to_str: {
+  key: 255
+  value: "0xff"
+}
+sfixed64_to_bool: {
+  key: 0
+  value: false
+}
+sfixed64_to_bool: {
+  key: 51966
+  value: true
+}
+bool_to_uint32: {
+  key: false
+  value: 101
+}
+bool_to_uint32: {
+  key: true
+  value: 42
+}
+`,
+	}, {
+		desc: "map fields 2",
+		input: M(&pb2.Maps{
+			Uint64ToEnum: map[uint64]pb2.Enum{
+				1:  pb2.Enum_FIRST,
+				2:  pb2.Enum_SECOND,
+				10: pb2.Enum_TENTH,
+			},
+		}),
+		want: `uint64_to_enum: {
+  key: 1
+  value: FIRST
+}
+uint64_to_enum: {
+  key: 2
+  value: SECOND
+}
+uint64_to_enum: {
+  key: 10
+  value: TENTH
+}
+`,
+	}, {
+		desc: "map fields 3",
+		input: M(&pb2.Maps{
+			StrToNested: map[string]*pb2.Nested{
+				"nested_one": &pb2.Nested{
+					OptString: scalar.String("nested in a map"),
+				},
+			},
+		}),
+		want: `str_to_nested: {
+  key: "nested_one"
+  value: {
+    opt_string: "nested in a map"
+  }
+}
+`,
+	}, {
+		desc: "map fields 4",
+		input: M(&pb2.Maps{
+			StrToOneofs: map[string]*pb2.Oneofs{
+				"string": &pb2.Oneofs{
+					Union: &pb2.Oneofs_Str{
+						Str: "hello",
+					},
+				},
+				"nested": &pb2.Oneofs{
+					Union: &pb2.Oneofs_Msg{
+						Msg: &pb2.Nested{
+							OptString: scalar.String("nested oneof in map field value"),
+						},
+					},
+				},
+			},
+		}),
+		want: `str_to_oneofs: {
+  key: "nested"
+  value: {
+    msg: {
+      opt_string: "nested oneof in map field value"
+    }
+  }
+}
+str_to_oneofs: {
+  key: "string"
+  value: {
+    str: "hello"
+  }
+}
+`,
+	}, {
+		desc:  "well-known type fields not set",
+		input: M(&pb2.KnownTypes{}),
+		want:  "\n",
+	}, {
+		desc: "well-known type fields set to empty messages",
+		input: M(&pb2.KnownTypes{
+			OptBool:      &wpb.BoolValue{},
+			OptInt32:     &wpb.Int32Value{},
+			OptInt64:     &wpb.Int64Value{},
+			OptUint32:    &wpb.UInt32Value{},
+			OptUint64:    &wpb.UInt64Value{},
+			OptFloat:     &wpb.FloatValue{},
+			OptDouble:    &wpb.DoubleValue{},
+			OptString:    &wpb.StringValue{},
+			OptBytes:     &wpb.BytesValue{},
+			OptDuration:  &durpb.Duration{},
+			OptTimestamp: &tspb.Timestamp{},
+			OptStruct:    &stpb.Struct{},
+			OptList:      &stpb.ListValue{},
+			OptValue:     &stpb.Value{},
+			OptEmpty:     &emptypb.Empty{},
+			OptAny:       &anypb.Any{},
+		}),
+		want: `opt_bool: {}
+opt_int32: {}
+opt_int64: {}
+opt_uint32: {}
+opt_uint64: {}
+opt_float: {}
+opt_double: {}
+opt_string: {}
+opt_bytes: {}
+opt_duration: {}
+opt_timestamp: {}
+opt_struct: {}
+opt_list: {}
+opt_value: {}
+opt_empty: {}
+opt_any: {}
+`,
+	}, {
+		desc: "well-known type scalar fields",
+		input: M(&pb2.KnownTypes{
+			OptBool: &wpb.BoolValue{
+				Value: true,
+			},
+			OptInt32: &wpb.Int32Value{
+				Value: -42,
+			},
+			OptInt64: &wpb.Int64Value{
+				Value: -42,
+			},
+			OptUint32: &wpb.UInt32Value{
+				Value: 0xff,
+			},
+			OptUint64: &wpb.UInt64Value{
+				Value: 0xffff,
+			},
+			OptFloat: &wpb.FloatValue{
+				Value: 1.234,
+			},
+			OptDouble: &wpb.DoubleValue{
+				Value: 1.23e308,
+			},
+			OptString: &wpb.StringValue{
+				Value: "谷歌",
+			},
+			OptBytes: &wpb.BytesValue{
+				Value: []byte("\xe8\xb0\xb7\xe6\xad\x8c"),
+			},
+		}),
+		want: `opt_bool: {
+  value: true
+}
+opt_int32: {
+  value: -42
+}
+opt_int64: {
+  value: -42
+}
+opt_uint32: {
+  value: 255
+}
+opt_uint64: {
+  value: 65535
+}
+opt_float: {
+  value: 1.2339999675750732
+}
+opt_double: {
+  value: 1.23e+308
+}
+opt_string: {
+  value: "谷歌"
+}
+opt_bytes: {
+  value: "谷歌"
+}
+`,
+	}, {
+		desc: "well-known type time-related fields",
+		input: M(&pb2.KnownTypes{
+			OptDuration: &durpb.Duration{
+				Seconds: -3600,
+				Nanos:   -123,
+			},
+			OptTimestamp: &tspb.Timestamp{
+				Seconds: 1257894000,
+				Nanos:   123,
+			},
+		}),
+		want: `opt_duration: {
+  seconds: -3600
+  nanos: -123
+}
+opt_timestamp: {
+  seconds: 1257894000
+  nanos: 123
+}
+`,
+	}, {
+		desc: "well-known type struct field and different Value types",
+		input: M(&pb2.KnownTypes{
+			OptStruct: &stpb.Struct{
+				Fields: map[string]*stpb.Value{
+					"bool": &stpb.Value{
+						Kind: &stpb.Value_BoolValue{
+							BoolValue: true,
+						},
+					},
+					"double": &stpb.Value{
+						Kind: &stpb.Value_NumberValue{
+							NumberValue: 3.1415,
+						},
+					},
+					"null": &stpb.Value{
+						Kind: &stpb.Value_NullValue{
+							NullValue: stpb.NullValue_NULL_VALUE,
+						},
+					},
+					"string": &stpb.Value{
+						Kind: &stpb.Value_StringValue{
+							StringValue: "string",
+						},
+					},
+					"struct": &stpb.Value{
+						Kind: &stpb.Value_StructValue{
+							StructValue: &stpb.Struct{
+								Fields: map[string]*stpb.Value{
+									"bool": &stpb.Value{
+										Kind: &stpb.Value_BoolValue{
+											BoolValue: false,
+										},
+									},
+								},
+							},
+						},
+					},
+					"list": &stpb.Value{
+						Kind: &stpb.Value_ListValue{
+							ListValue: &stpb.ListValue{
+								Values: []*stpb.Value{
+									{
+										Kind: &stpb.Value_BoolValue{
+											BoolValue: false,
+										},
+									},
+									{
+										Kind: &stpb.Value_StringValue{
+											StringValue: "hello",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}),
+		want: `opt_struct: {
+  fields: {
+    key: "bool"
+    value: {
+      bool_value: true
+    }
+  }
+  fields: {
+    key: "double"
+    value: {
+      number_value: 3.1415
+    }
+  }
+  fields: {
+    key: "list"
+    value: {
+      list_value: {
+        values: {
+          bool_value: false
+        }
+        values: {
+          string_value: "hello"
+        }
+      }
+    }
+  }
+  fields: {
+    key: "null"
+    value: {
+      null_value: NULL_VALUE
+    }
+  }
+  fields: {
+    key: "string"
+    value: {
+      string_value: "string"
+    }
+  }
+  fields: {
+    key: "struct"
+    value: {
+      struct_value: {
+        fields: {
+          key: "bool"
+          value: {
+            bool_value: false
+          }
+        }
+      }
+    }
+  }
+}
+`,
+	}}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+			want := tt.want
+			b, err := textpb.Marshal(tt.input)
+			if err != nil && !tt.wantErr {
+				t.Errorf("Marshal() returned error: %v\n\n", err)
+			}
+			if tt.wantErr && err == nil {
+				t.Errorf("Marshal() got nil error, want error\n\n")
+			}
+			if got := string(b); got != want {
+				t.Errorf("Marshal()\n<got>\n%v\n<want>\n%v\n", got, want)
+				if diff := cmp.Diff(want, got, splitLines); diff != "" {
+					t.Errorf("Marshal() diff -want +got\n%v\n", diff)
+				}
+			}
+		})
+	}
+}