| /* | |
| Copyright 2017 Google LLC | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| package spanner | |
| import ( | |
| "math/big" | |
| "sort" | |
| "strings" | |
| "testing" | |
| "time" | |
| "cloud.google.com/go/civil" | |
| sppb "cloud.google.com/go/spanner/apiv1/spannerpb" | |
| proto3 "github.com/golang/protobuf/ptypes/struct" | |
| ) | |
| // keysetProto returns protobuf encoding of valid spanner.KeySet. | |
| func keysetProto(t *testing.T, ks KeySet) *sppb.KeySet { | |
| k, err := ks.keySetProto() | |
| if err != nil { | |
| t.Fatalf("cannot convert keyset %v to protobuf: %v", ks, err) | |
| } | |
| return k | |
| } | |
| // Test encoding from spanner.Mutation to protobuf. | |
| func TestMutationToProto(t *testing.T) { | |
| utc, err := time.LoadLocation("UTC") | |
| if err != nil { | |
| t.Fatalf("Could not load UTC: %v", err) | |
| } | |
| r, _ := (&big.Rat{}).SetString("3.14") | |
| for i, test := range []struct { | |
| m *Mutation | |
| want *sppb.Mutation | |
| }{ | |
| // Delete Mutation | |
| { | |
| &Mutation{opDelete, "t_foo", Key{"foo"}, nil, nil}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Delete_{ | |
| Delete: &sppb.Mutation_Delete{ | |
| Table: "t_foo", | |
| KeySet: keysetProto(t, Key{"foo"}), | |
| }, | |
| }, | |
| }, | |
| }, | |
| // Insert Mutation | |
| { | |
| &Mutation{opInsert, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Insert{ | |
| Insert: &sppb.Mutation_Write{ | |
| Table: "t_foo", | |
| Columns: []string{"col1", "col2"}, | |
| Values: []*proto3.ListValue{ | |
| { | |
| Values: []*proto3.Value{intProto(1), intProto(2)}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| // InsertOrUpdate Mutation | |
| { | |
| &Mutation{opInsertOrUpdate, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{1.0, 2.0}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_InsertOrUpdate{ | |
| InsertOrUpdate: &sppb.Mutation_Write{ | |
| Table: "t_foo", | |
| Columns: []string{"col1", "col2"}, | |
| Values: []*proto3.ListValue{ | |
| { | |
| Values: []*proto3.Value{floatProto(1.0), floatProto(2.0)}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| // Replace Mutation | |
| { | |
| &Mutation{opReplace, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{"one", 2.0}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Replace{ | |
| Replace: &sppb.Mutation_Write{ | |
| Table: "t_foo", | |
| Columns: []string{"col1", "col2"}, | |
| Values: []*proto3.ListValue{ | |
| { | |
| Values: []*proto3.Value{stringProto("one"), floatProto(2.0)}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| // Update Mutation | |
| { | |
| &Mutation{opUpdate, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Update{ | |
| Update: &sppb.Mutation_Write{ | |
| Table: "t_foo", | |
| Columns: []string{"col1", "col2"}, | |
| Values: []*proto3.ListValue{ | |
| { | |
| Values: []*proto3.Value{stringProto("one"), nullProto()}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| // Mutation with all supported data types | |
| { | |
| &Mutation{ | |
| opInsert, | |
| "t_foo", | |
| KeySets(), | |
| []string{"colBool", "colInt64", "colFloat64", "colNumeric", "colString", "colBytes", "colDate", "colTimestamp"}, | |
| []interface{}{ | |
| true, | |
| int64(100), | |
| float64(3.14), | |
| *r, // 3.14 | |
| "one", | |
| []byte{1, 2, 3}, | |
| civil.Date{Year: 2020, Month: 12, Day: 2}, | |
| time.Date(2020, time.December, 3, 8, 46, 58, 109, utc), | |
| }, | |
| }, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Insert{ | |
| Insert: &sppb.Mutation_Write{ | |
| Table: "t_foo", | |
| Columns: []string{"colBool", "colInt64", "colFloat64", "colNumeric", "colString", "colBytes", "colDate", "colTimestamp"}, | |
| Values: []*proto3.ListValue{ | |
| { | |
| Values: []*proto3.Value{ | |
| boolProto(true), | |
| stringProto("100"), | |
| floatProto(3.14), | |
| stringProto("3.140000000"), | |
| stringProto("one"), | |
| bytesProto([]byte{1, 2, 3}), | |
| stringProto("2020-12-02"), | |
| stringProto("2020-12-03T08:46:58.000000109Z"), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } { | |
| if got, err := test.m.proto(); err != nil || !testEqual(got, test.want) { | |
| t.Errorf("%d:\n(%#v).proto() =\n (%v, %v)\nwant (%v, nil)", i, test.m, got, err, test.want) | |
| } | |
| } | |
| } | |
| // mutationColumnSorter implements sort.Interface for sorting column-value pairs in a Mutation by column names. | |
| type mutationColumnSorter struct { | |
| Mutation | |
| } | |
| // newMutationColumnSorter creates new instance of mutationColumnSorter by duplicating the input Mutation so that | |
| // sorting won't change the input Mutation. | |
| func newMutationColumnSorter(m *Mutation) *mutationColumnSorter { | |
| return &mutationColumnSorter{ | |
| Mutation{ | |
| m.op, | |
| m.table, | |
| m.keySet, | |
| append([]string(nil), m.columns...), | |
| append([]interface{}(nil), m.values...), | |
| }, | |
| } | |
| } | |
| // Len implements sort.Interface.Len. | |
| func (ms *mutationColumnSorter) Len() int { | |
| return len(ms.columns) | |
| } | |
| // Swap implements sort.Interface.Swap. | |
| func (ms *mutationColumnSorter) Swap(i, j int) { | |
| ms.columns[i], ms.columns[j] = ms.columns[j], ms.columns[i] | |
| ms.values[i], ms.values[j] = ms.values[j], ms.values[i] | |
| } | |
| // Less implements sort.Interface.Less. | |
| func (ms *mutationColumnSorter) Less(i, j int) bool { | |
| return strings.Compare(ms.columns[i], ms.columns[j]) < 0 | |
| } | |
| // mutationEqual returns true if two mutations in question are equal | |
| // to each other. | |
| func mutationEqual(t *testing.T, m1, m2 Mutation) bool { | |
| // Two mutations are considered to be equal even if their column values have different | |
| // orders. | |
| ms1 := newMutationColumnSorter(&m1) | |
| ms2 := newMutationColumnSorter(&m2) | |
| sort.Sort(ms1) | |
| sort.Sort(ms2) | |
| return testEqual(ms1, ms2) | |
| } | |
| // Test helper functions which help to generate spanner.Mutation. | |
| func TestMutationHelpers(t *testing.T) { | |
| for _, test := range []struct { | |
| m string | |
| got *Mutation | |
| want *Mutation | |
| }{ | |
| { | |
| "Insert", | |
| Insert("t_foo", []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}), | |
| &Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}}, | |
| }, | |
| { | |
| "InsertMap", | |
| InsertMap("t_foo", map[string]interface{}{"col1": int64(1), "col2": int64(2)}), | |
| &Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}}, | |
| }, | |
| { | |
| "InsertStruct", | |
| func() *Mutation { | |
| m, err := InsertStruct( | |
| "t_foo", | |
| struct { | |
| notCol bool | |
| Col1 int64 `spanner:"col1"` | |
| Col2 int64 `spanner:"col2"` | |
| }{false, int64(1), int64(2)}, | |
| ) | |
| if err != nil { | |
| t.Errorf("cannot convert struct into mutation: %v", err) | |
| } | |
| return m | |
| }(), | |
| &Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}}, | |
| }, | |
| { | |
| "Update", | |
| Update("t_foo", []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}), | |
| &Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}}, | |
| }, | |
| { | |
| "UpdateMap", | |
| UpdateMap("t_foo", map[string]interface{}{"col1": "one", "col2": []byte(nil)}), | |
| &Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}}, | |
| }, | |
| { | |
| "UpdateStruct", | |
| func() *Mutation { | |
| m, err := UpdateStruct( | |
| "t_foo", | |
| struct { | |
| Col1 string `spanner:"col1"` | |
| notCol int | |
| Col2 []byte `spanner:"col2"` | |
| }{"one", 1, nil}, | |
| ) | |
| if err != nil { | |
| t.Errorf("cannot convert struct into mutation: %v", err) | |
| } | |
| return m | |
| }(), | |
| &Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}}, | |
| }, | |
| { | |
| "InsertOrUpdate", | |
| InsertOrUpdate("t_foo", []string{"col1", "col2"}, []interface{}{1.0, 2.0}), | |
| &Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}}, | |
| }, | |
| { | |
| "InsertOrUpdateMap", | |
| InsertOrUpdateMap("t_foo", map[string]interface{}{"col1": 1.0, "col2": 2.0}), | |
| &Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}}, | |
| }, | |
| { | |
| "InsertOrUpdateStruct", | |
| func() *Mutation { | |
| m, err := InsertOrUpdateStruct( | |
| "t_foo", | |
| struct { | |
| Col1 float64 `spanner:"col1"` | |
| Col2 float64 `spanner:"col2"` | |
| notCol float64 | |
| }{1.0, 2.0, 3.0}, | |
| ) | |
| if err != nil { | |
| t.Errorf("cannot convert struct into mutation: %v", err) | |
| } | |
| return m | |
| }(), | |
| &Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}}, | |
| }, | |
| { | |
| "Replace", | |
| Replace("t_foo", []string{"col1", "col2"}, []interface{}{"one", 2.0}), | |
| &Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}}, | |
| }, | |
| { | |
| "ReplaceMap", | |
| ReplaceMap("t_foo", map[string]interface{}{"col1": "one", "col2": 2.0}), | |
| &Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}}, | |
| }, | |
| { | |
| "ReplaceStruct", | |
| func() *Mutation { | |
| m, err := ReplaceStruct( | |
| "t_foo", | |
| struct { | |
| Col1 string `spanner:"col1"` | |
| Col2 float64 `spanner:"col2"` | |
| notCol string | |
| }{"one", 2.0, "foo"}, | |
| ) | |
| if err != nil { | |
| t.Errorf("cannot convert struct into mutation: %v", err) | |
| } | |
| return m | |
| }(), | |
| &Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}}, | |
| }, | |
| { | |
| "Delete", | |
| Delete("t_foo", Key{"foo"}), | |
| &Mutation{opDelete, "t_foo", Key{"foo"}, nil, nil}, | |
| }, | |
| { | |
| "DeleteRange", | |
| Delete("t_foo", KeyRange{Key{"bar"}, Key{"foo"}, ClosedClosed}), | |
| &Mutation{opDelete, "t_foo", KeyRange{Key{"bar"}, Key{"foo"}, ClosedClosed}, nil, nil}, | |
| }, | |
| } { | |
| if !mutationEqual(t, *test.got, *test.want) { | |
| t.Errorf("%v: got Mutation %v, want %v", test.m, test.got, test.want) | |
| } | |
| } | |
| } | |
| // Test encoding non-struct types by using *Struct helpers. | |
| func TestBadStructs(t *testing.T) { | |
| val := "i_am_not_a_struct" | |
| wantErr := errNotStruct(val) | |
| if _, gotErr := InsertStruct("t_test", val); !testEqual(gotErr, wantErr) { | |
| t.Errorf("InsertStruct(%q) returns error %v, want %v", val, gotErr, wantErr) | |
| } | |
| if _, gotErr := InsertOrUpdateStruct("t_test", val); !testEqual(gotErr, wantErr) { | |
| t.Errorf("InsertOrUpdateStruct(%q) returns error %v, want %v", val, gotErr, wantErr) | |
| } | |
| if _, gotErr := UpdateStruct("t_test", val); !testEqual(gotErr, wantErr) { | |
| t.Errorf("UpdateStruct(%q) returns error %v, want %v", val, gotErr, wantErr) | |
| } | |
| if _, gotErr := ReplaceStruct("t_test", val); !testEqual(gotErr, wantErr) { | |
| t.Errorf("ReplaceStruct(%q) returns error %v, want %v", val, gotErr, wantErr) | |
| } | |
| } | |
| func TestStructToMutationParams(t *testing.T) { | |
| // Tests cases not covered elsewhere. | |
| type S struct{ F interface{} } | |
| for _, test := range []struct { | |
| in interface{} | |
| wantCols []string | |
| wantVals []interface{} | |
| wantErr error | |
| }{ | |
| {nil, nil, nil, errNotStruct(nil)}, | |
| {3, nil, nil, errNotStruct(3)}, | |
| {(*S)(nil), nil, nil, nil}, | |
| {&S{F: 1}, []string{"F"}, []interface{}{1}, nil}, | |
| {&S{F: CommitTimestamp}, []string{"F"}, []interface{}{CommitTimestamp}, nil}, | |
| } { | |
| gotCols, gotVals, gotErr := structToMutationParams(test.in) | |
| if !testEqual(gotCols, test.wantCols) { | |
| t.Errorf("%#v: got cols %v, want %v", test.in, gotCols, test.wantCols) | |
| } | |
| if !testEqual(gotVals, test.wantVals) { | |
| t.Errorf("%#v: got vals %v, want %v", test.in, gotVals, test.wantVals) | |
| } | |
| if !testEqual(gotErr, test.wantErr) { | |
| t.Errorf("%#v: got err %v, want %v", test.in, gotErr, test.wantErr) | |
| } | |
| } | |
| } | |
| // Test encoding Mutation into proto. | |
| func TestEncodeMutation(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| mutation Mutation | |
| wantProto *sppb.Mutation | |
| wantErr error | |
| }{ | |
| { | |
| "OpDelete", | |
| Mutation{opDelete, "t_test", Key{1}, nil, nil}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Delete_{ | |
| Delete: &sppb.Mutation_Delete{ | |
| Table: "t_test", | |
| KeySet: &sppb.KeySet{ | |
| Keys: []*proto3.ListValue{listValueProto(intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "OpDelete - Key error", | |
| Mutation{opDelete, "t_test", Key{struct{}{}}, nil, nil}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Delete_{ | |
| Delete: &sppb.Mutation_Delete{ | |
| Table: "t_test", | |
| KeySet: &sppb.KeySet{}, | |
| }, | |
| }, | |
| }, | |
| errInvdKeyPartType(struct{}{}), | |
| }, | |
| { | |
| "OpInsert", | |
| Mutation{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Insert{ | |
| Insert: &sppb.Mutation_Write{ | |
| Table: "t_test", | |
| Columns: []string{"key", "val"}, | |
| Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "OpInsert - Value Type Error", | |
| Mutation{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Insert{ | |
| Insert: &sppb.Mutation_Write{}, | |
| }, | |
| }, | |
| errEncoderUnsupportedType(struct{}{}), | |
| }, | |
| { | |
| "OpInsertOrUpdate", | |
| Mutation{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_InsertOrUpdate{ | |
| InsertOrUpdate: &sppb.Mutation_Write{ | |
| Table: "t_test", | |
| Columns: []string{"key", "val"}, | |
| Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "OpInsertOrUpdate - Value Type Error", | |
| Mutation{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_InsertOrUpdate{ | |
| InsertOrUpdate: &sppb.Mutation_Write{}, | |
| }, | |
| }, | |
| errEncoderUnsupportedType(struct{}{}), | |
| }, | |
| { | |
| "OpReplace", | |
| Mutation{opReplace, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Replace{ | |
| Replace: &sppb.Mutation_Write{ | |
| Table: "t_test", | |
| Columns: []string{"key", "val"}, | |
| Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "OpReplace - Value Type Error", | |
| Mutation{opReplace, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Replace{ | |
| Replace: &sppb.Mutation_Write{}, | |
| }, | |
| }, | |
| errEncoderUnsupportedType(struct{}{}), | |
| }, | |
| { | |
| "OpUpdate", | |
| Mutation{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Update{ | |
| Update: &sppb.Mutation_Write{ | |
| Table: "t_test", | |
| Columns: []string{"key", "val"}, | |
| Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "OpUpdate - Value Type Error", | |
| Mutation{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}}, | |
| &sppb.Mutation{ | |
| Operation: &sppb.Mutation_Update{ | |
| Update: &sppb.Mutation_Write{}, | |
| }, | |
| }, | |
| errEncoderUnsupportedType(struct{}{}), | |
| }, | |
| { | |
| "OpKnown - Unknown Mutation Operation Code", | |
| Mutation{op(100), "t_test", nil, nil, nil}, | |
| &sppb.Mutation{}, | |
| errInvdMutationOp(Mutation{op(100), "t_test", nil, nil, nil}), | |
| }, | |
| } { | |
| gotProto, gotErr := test.mutation.proto() | |
| if gotErr != nil { | |
| if !testEqual(gotErr, test.wantErr) { | |
| t.Errorf("%s: %v.proto() returns error %v, want %v", test.name, test.mutation, gotErr, test.wantErr) | |
| } | |
| continue | |
| } | |
| if !testEqual(gotProto, test.wantProto) { | |
| t.Errorf("%s: %v.proto() = (%v, nil), want (%v, nil)", test.name, test.mutation, gotProto, test.wantProto) | |
| } | |
| } | |
| } | |
| // Test Encoding an array of mutations. | |
| func TestEncodeMutationArray(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| ms []*Mutation | |
| want []*sppb.Mutation | |
| wantErr error | |
| }{ | |
| { | |
| "Multiple Mutations", | |
| []*Mutation{ | |
| {opDelete, "t_test", Key{"bar"}, nil, nil}, | |
| {opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}}, | |
| }, | |
| []*sppb.Mutation{ | |
| { | |
| Operation: &sppb.Mutation_Delete_{ | |
| Delete: &sppb.Mutation_Delete{ | |
| Table: "t_test", | |
| KeySet: &sppb.KeySet{ | |
| Keys: []*proto3.ListValue{listValueProto(stringProto("bar"))}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| Operation: &sppb.Mutation_InsertOrUpdate{ | |
| InsertOrUpdate: &sppb.Mutation_Write{ | |
| Table: "t_test", | |
| Columns: []string{"key", "val"}, | |
| Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| nil, | |
| }, | |
| { | |
| "Multiple Mutations - Bad Mutation", | |
| []*Mutation{ | |
| {opDelete, "t_test", Key{"bar"}, nil, nil}, | |
| {opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", struct{}{}}}, | |
| }, | |
| []*sppb.Mutation{}, | |
| errEncoderUnsupportedType(struct{}{}), | |
| }, | |
| } { | |
| gotProto, gotErr := mutationsProto(test.ms) | |
| if gotErr != nil { | |
| if !testEqual(gotErr, test.wantErr) { | |
| t.Errorf("%v: mutationsProto(%v) returns error %v, want %v", test.name, test.ms, gotErr, test.wantErr) | |
| } | |
| continue | |
| } | |
| if !testEqual(gotProto, test.want) { | |
| t.Errorf("%v: mutationsProto(%v) = (%v, nil), want (%v, nil)", test.name, test.ms, gotProto, test.want) | |
| } | |
| } | |
| } |