go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/proto/msgpackpb/generic_test.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package msgpackpb
    16  
    17  import (
    18  	"bytes"
    19  	"math"
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	"github.com/vmihailenco/msgpack/v5"
    26  	. "go.chromium.org/luci/common/testing/assertions"
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/reflect/protoreflect"
    29  	"google.golang.org/protobuf/types/known/durationpb"
    30  )
    31  
    32  func TestRoundtrip(t *testing.T) {
    33  	t.Parallel()
    34  
    35  	testCases := []struct {
    36  		name    string
    37  		input   *TestMessage
    38  		err     string
    39  		raw     []byte
    40  		options []Option
    41  	}{
    42  		{
    43  			name: "scalar fields",
    44  			input: &TestMessage{
    45  				Boolval:       true,
    46  				Intval:        -100,
    47  				Uintval:       100,
    48  				ShortIntval:   -50,
    49  				ShortUintval:  50,
    50  				Floatval:      6.28318531,
    51  				ShortFloatval: 3.1415,
    52  				Strval:        "hi",
    53  				Value:         VALUE_ONE,
    54  			},
    55  			raw: []byte{
    56  				137,    // 9 element map
    57  				2, 195, // tag 2, true
    58  				3, 208, 156, // tag 3, -100
    59  				4, 100, // tag 4, 100
    60  				5, 208, 206, // tag 5, -50
    61  				6, 50, // tag 6, 50
    62  				7, 162, 104, 105, // tag 7, "hi"
    63  				8, 203, 64, 25, 33, 251, 84, 116, 161, 104, // tag 8, 6.28318531
    64  				9, 202, 64, 73, 14, 86, // tag 9, 3.1415
    65  				10, 1}, // tag 10, 1
    66  			options: []Option{Deterministic},
    67  		},
    68  
    69  		{
    70  			name: "repeated simple",
    71  			input: &TestMessage{
    72  				Strings: []string{"hello", "there"},
    73  			},
    74  			raw: []byte{
    75  				129,     // 1 element map
    76  				13, 146, // tag 13, 2 element array
    77  				165, 104, 101, 108, 108, 111, // "hello"
    78  				165, 116, 104, 101, 114, 101, // "there"
    79  			},
    80  			options: []Option{Deterministic},
    81  		},
    82  
    83  		{
    84  			name: "embedded message",
    85  			input: &TestMessage{
    86  				SingleRecurse: &TestMessage{
    87  					SingleRecurse: &TestMessage{
    88  						Strval: "hello",
    89  					},
    90  				},
    91  			},
    92  			raw: []byte{
    93  				129,                             // 1 element map
    94  				14,                              // tag 13
    95  				129,                             // 1 element map
    96  				14,                              // tag 13
    97  				129,                             // 1 element map
    98  				7, 165, 104, 101, 108, 108, 111, // tag 7, "hello"
    99  			},
   100  			options: []Option{Deterministic},
   101  		},
   102  
   103  		{
   104  			name: "external message",
   105  			input: &TestMessage{
   106  				Duration: &durationpb.Duration{
   107  					Seconds: 10000,
   108  					Nanos:   10000,
   109  				},
   110  			},
   111  			raw: []byte{
   112  				129,     // 1 element map
   113  				12, 146, // tag 12, 2 element ARRAY, since this message is encoded like a lua 'array'
   114  				205, 39, 16, // (implicit tag 1), 10000
   115  				205, 39, 16, // (implicit tag 2), 10000
   116  			},
   117  			options: []Option{Deterministic},
   118  		},
   119  
   120  		{
   121  			name: "map",
   122  			input: &TestMessage{
   123  				Mapfield: map[string]*TestMessage{
   124  					"hello":   {Strval: "there"},
   125  					"general": {Strval: "kinobi..."},
   126  				},
   127  			},
   128  			raw: []byte{
   129  				129,     // 1 element map
   130  				11, 130, // tag 11, 2 entry map
   131  				167, 103, 101, 110, 101, 114, 97, 108, // "general"
   132  				129,                                             // 2 element map
   133  				7, 169, 107, 105, 110, 111, 98, 105, 46, 46, 46, // tag 7, "kenobi..."
   134  				165, 104, 101, 108, 108, 111, // "hello"
   135  				129,                             // 1 element map
   136  				7, 165, 116, 104, 101, 114, 101, // tag 7, "there"
   137  			},
   138  			options: []Option{Deterministic},
   139  		},
   140  
   141  		{
   142  			name: "intern",
   143  			input: &TestMessage{
   144  				Strval: "am interned",
   145  				Mapfield: map[string]*TestMessage{
   146  					"another": {Boolval: true},
   147  					"not":     {Boolval: false},
   148  				},
   149  				SingleRecurse: &TestMessage{
   150  					Strval: "also not",
   151  				},
   152  			},
   153  			raw: []byte{
   154  				131,  // 3 element map
   155  				7, 0, // tag 7, interned string 0
   156  				11, 130, // tag 11, 2 element map
   157  				1, 129, 2, 195, // interned string 1, 1 element map, tag 2, true
   158  				163, 110, 111, 116, 128, // "not", zero element map.
   159  				14, 129, // tag 14, 1 element map
   160  				7, 168, 97, 108, 115, 111, 32, 110, 111, 116, // tag 7, "also not"
   161  			},
   162  			options: []Option{Deterministic, WithStringInternTable([]string{
   163  				"am interned",
   164  				"another",
   165  			})},
   166  		},
   167  	}
   168  
   169  	Convey(`TestRoundtrip`, t, func() {
   170  		for _, tc := range testCases {
   171  			tc := tc
   172  			Convey(tc.name, func() {
   173  				raw, err := Marshal(tc.input, tc.options...)
   174  				if tc.err == "" {
   175  					So(err, ShouldBeNil)
   176  				} else {
   177  					So(err, ShouldErrLike, tc.err)
   178  					return
   179  				}
   180  
   181  				if tc.raw != nil {
   182  					So([]byte(raw), ShouldResemble, tc.raw)
   183  				}
   184  
   185  				msg := &TestMessage{}
   186  				So(Unmarshal(raw, msg, tc.options...), ShouldBeNil)
   187  
   188  				So(msg, ShouldResembleProto, tc.input)
   189  			})
   190  		}
   191  	})
   192  
   193  }
   194  
   195  func TestEncode(t *testing.T) {
   196  	t.Parallel()
   197  
   198  	Convey(`TestEncode`, t, func() {
   199  		Convey(`unknown fields`, func() {
   200  			// use Duration which encodes seconds with field 1, which is reserved.
   201  			enc, err := proto.Marshal(durationpb.New(20 * time.Second))
   202  			So(err, ShouldBeNil)
   203  
   204  			tm := &TestMessage{}
   205  			So(proto.Unmarshal(enc, tm), ShouldBeNil)
   206  
   207  			So(tm.ProtoReflect().GetUnknown(), ShouldNotBeEmpty)
   208  
   209  			_, err = Marshal(tm)
   210  			So(err, ShouldErrLike, "unknown non-msgpack fields")
   211  		})
   212  	})
   213  }
   214  
   215  // TestDecode tests the pathway from msgpack -> proto, focusing on pathways
   216  // where the msgpack message contains a different encoded value than the target
   217  // field.
   218  func TestDecode(t *testing.T) {
   219  	t.Parallel()
   220  
   221  	testCases := []struct {
   222  		name          string
   223  		tweakEnc      func(*msgpack.Encoder)
   224  		input         any // will be encoded verbatim with
   225  		expect        *TestMessage
   226  		expectUnknown protoreflect.RawFields
   227  		expectRaw     msgpack.RawMessage
   228  		expectDecoded any
   229  		err           string
   230  	}{
   231  		{
   232  			name: "int32->int64",
   233  			input: map[int32]any{
   234  				3: int32(10),
   235  			},
   236  			expect: &TestMessage{Intval: 10},
   237  		},
   238  		{
   239  			name: "int8->int64",
   240  			input: map[int32]any{
   241  				3: int8(10),
   242  			},
   243  			expect: &TestMessage{Intval: 10},
   244  		},
   245  		{
   246  			name: "int64->int32",
   247  			input: map[int32]any{
   248  				5: int64(10),
   249  			},
   250  			expect: &TestMessage{ShortIntval: 10},
   251  		},
   252  		{
   253  			name: "int64->int32 (overflow)",
   254  			input: map[int32]any{
   255  				5: int64(math.MaxInt32 * 2),
   256  			},
   257  			expect: &TestMessage{ShortIntval: -2},
   258  		},
   259  		{
   260  			name: "float64->int32",
   261  			input: map[int32]any{
   262  				5: float64(217),
   263  			},
   264  			err: "bad type: expected int32, got float64",
   265  		},
   266  
   267  		{
   268  			name: "unknown field",
   269  			input: map[int32]any{
   270  				777: "nerds",
   271  				3:   100,
   272  			},
   273  			expect: &TestMessage{
   274  				Intval: 100,
   275  			},
   276  			expectUnknown: []byte{
   277  				250, 255, 255, 255, 15, // proto: 536870911: LEN
   278  				10,        // proto: 10 bytes in this field
   279  				129,       // msgpack: 1 element map
   280  				205, 3, 9, // msgpack: 777
   281  				165, 110, 101, 114, 100, 115, // msgpack: 5-char string, "nerds"
   282  			},
   283  			expectRaw: []byte{
   284  				130,    // 2 item map
   285  				3, 100, // tag 3, 100
   286  				205, 3, 9, 165, 110, 101, 114, 100, 115, // tag 777, 5 char string "nerds"
   287  			},
   288  			expectDecoded: map[int32]any{
   289  				3:   int64(100),
   290  				777: "nerds",
   291  			},
   292  		},
   293  
   294  		{
   295  			name: "sparse array",
   296  			input: map[int32]any{
   297  				13: map[int32]string{
   298  					3:  "hello",
   299  					12: "there",
   300  				},
   301  			},
   302  			expect: &TestMessage{
   303  				Strings: []string{
   304  					"", "", "",
   305  					"hello",
   306  					"", "", "",
   307  					"", "", "",
   308  					"", "",
   309  					"there",
   310  				},
   311  			},
   312  		},
   313  	}
   314  
   315  	Convey(`TestDecode`, t, func() {
   316  		for _, tc := range testCases {
   317  			tc := tc
   318  			Convey(tc.name, func() {
   319  				enc := msgpack.GetEncoder()
   320  				defer msgpack.PutEncoder(enc)
   321  
   322  				buf := bytes.Buffer{}
   323  				enc.Reset(&buf)
   324  				if tc.tweakEnc != nil {
   325  					tc.tweakEnc(enc)
   326  				}
   327  				So(enc.Encode(tc.input), ShouldBeNil)
   328  
   329  				msg := &TestMessage{}
   330  				err := Unmarshal(buf.Bytes(), msg)
   331  				if tc.err == "" {
   332  					So(err, ShouldBeNil)
   333  
   334  					known := proto.Clone(msg).(*TestMessage)
   335  					known.ProtoReflect().SetUnknown(nil)
   336  					So(known, ShouldResembleProto, tc.expect)
   337  
   338  					So(msg.ProtoReflect().GetUnknown(), ShouldResemble, tc.expectUnknown)
   339  
   340  					if tc.expectRaw != nil {
   341  						raw, err := Marshal(msg, Deterministic)
   342  						So(err, ShouldBeNil)
   343  
   344  						So(raw, ShouldResemble, tc.expectRaw)
   345  
   346  						if len(msg.ProtoReflect().GetUnknown()) > 0 {
   347  							dec := msgpack.GetDecoder()
   348  							defer msgpack.PutDecoder(dec)
   349  							dec.Reset(bytes.NewBuffer(raw))
   350  							dec.UseLooseInterfaceDecoding(true)
   351  							dec.SetMapDecoder(func(d *msgpack.Decoder) (any, error) {
   352  								return d.DecodeUntypedMap()
   353  							})
   354  
   355  							decoded := reflect.MakeMap(reflect.TypeOf(tc.expectDecoded))
   356  
   357  							So(dec.DecodeValue(decoded), ShouldBeNil)
   358  
   359  							So(decoded.Interface(), ShouldResemble, tc.expectDecoded)
   360  						}
   361  					}
   362  				} else {
   363  					So(err, ShouldErrLike, tc.err)
   364  				}
   365  
   366  			})
   367  		}
   368  	})
   369  
   370  }