blob: d9353c165b1f19cb6376d84a07a172dde1181ae5 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// 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 mask
import (
"testing"
"github.com/golang/protobuf/proto"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/protobuf/reflect/protoreflect"
"go.chromium.org/luci/common/proto/internal/testingpb"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
type testMsg = testingpb.Full
var testMsgDescriptor = proto.MessageReflect(&testMsg{}).Descriptor()
func TestNormalizePath(t *testing.T) {
Convey("Retrun empty paths when given empty paths", t, func() {
So(normalizePaths([]path{}), ShouldResemble, []path{})
})
Convey("Remove all deduplicate paths", t, func() {
So(normalizePaths([]path{
{"a", "b"},
{"a", "b"},
{"a", "b"},
}), ShouldResemble, []path{
{"a", "b"},
})
})
Convey("Remove all redundant paths and return sorted", t, func() {
So(normalizePaths([]path{
{"b", "z"},
{"b", "c", "d"},
{"b", "c"},
{"a"},
}), ShouldResemble, []path{
{"a"},
{"b", "c"},
{"b", "z"},
})
})
}
func TestFromFieldMask(t *testing.T) {
Convey("From", t, func() {
parse := func(paths []string, isUpdateMask bool) (*Mask, error) {
return FromFieldMask(&field_mask.FieldMask{Paths: paths}, &testMsg{}, false, isUpdateMask)
}
Convey("empty field mask", func() {
actual, err := parse([]string{}, false)
So(err, ShouldBeNil)
assertMaskEqual(actual, &Mask{
descriptor: testMsgDescriptor,
})
})
Convey("field mask with scalar and message fields", func() {
actual, err := parse([]string{"str", "num", "msg.num"}, false)
So(err, ShouldBeNil)
assertMaskEqual(actual, &Mask{
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"str": &Mask{},
"num": &Mask{},
"msg": {
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"num": &Mask{},
},
},
},
})
})
Convey("field mask with map field", func() {
actual, err := parse([]string{"map_str_msg.some_key.str", "map_str_num.another_key"}, false)
So(err, ShouldBeNil)
assertMaskEqual(actual, &Mask{
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"map_str_msg": &Mask{
descriptor: testMsgDescriptor.Fields().ByName(protoreflect.Name("map_str_msg")).Message(),
isRepeated: true,
children: map[string]*Mask{
"some_key": &Mask{
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"str": &Mask{},
},
},
},
},
"map_str_num": {
descriptor: testMsgDescriptor.Fields().ByName(protoreflect.Name("map_str_num")).Message(),
isRepeated: true,
children: map[string]*Mask{
"another_key": &Mask{},
},
},
},
})
})
Convey("field mask with repeated field", func() {
actual, err := parse([]string{"nums", "msgs.*.str"}, false)
So(err, ShouldBeNil)
assertMaskEqual(actual, &Mask{
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"msgs": {
descriptor: testMsgDescriptor,
isRepeated: true,
children: map[string]*Mask{
"*": &Mask{
descriptor: testMsgDescriptor,
children: map[string]*Mask{
"str": &Mask{},
},
},
},
},
"nums": &Mask{
isRepeated: true,
},
},
})
})
Convey("update mask", func() {
_, err := parse([]string{"msgs.*.str"}, true)
So(err, ShouldErrLike, "update mask allows a repeated field only at the last position; field: msgs is not last")
_, err = parse([]string{"map_str_msg.*.str"}, true)
So(err, ShouldErrLike, "update mask allows a repeated field only at the last position; field: map_str_msg is not last")
})
})
}
func TestTrim(t *testing.T) {
Convey("Test", t, func() {
testTrim := func(maskPaths []string, msg proto.Message) {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: maskPaths}, &testMsg{}, false, false)
So(err, ShouldBeNil)
err = m.Trim(msg)
So(err, ShouldBeNil)
}
Convey("trim scalar field", func() {
msg := &testMsg{Num: 1}
testTrim([]string{"str"}, msg)
So(msg, ShouldResembleProto, &testMsg{})
})
Convey("keep scalar field", func() {
msg := &testMsg{Num: 1}
testTrim([]string{"num"}, msg)
So(msg, ShouldResembleProto, &testMsg{Num: 1})
})
Convey("trim repeated scalar field", func() {
msg := &testMsg{Nums: []int32{1, 2}}
testTrim([]string{"str"}, msg)
So(msg, ShouldResembleProto, &testMsg{})
})
Convey("keep repeated scalar field", func() {
msg := &testMsg{Nums: []int32{1, 2}}
testTrim([]string{"nums"}, msg)
So(msg, ShouldResembleProto, &testMsg{Nums: []int32{1, 2}})
})
Convey("trim submessage", func() {
msg := &testMsg{
Msg: &testMsg{
Num: 1,
},
}
testTrim([]string{"str"}, msg)
So(msg, ShouldResembleProto, &testMsg{})
})
Convey("keep submessage entirely", func() {
msg := &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
}
testTrim([]string{"msg"}, msg)
So(msg, ShouldResembleProto, &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
})
})
Convey("keep submessage partially", func() {
msg := &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
}
testTrim([]string{"msg.str"}, msg)
So(msg, ShouldResembleProto, &testMsg{
Msg: &testMsg{
Str: "abc",
},
})
})
Convey("trim repeated message field", func() {
msg := &testMsg{
Msgs: []*testMsg{
{Num: 1},
{Num: 2},
},
}
testTrim([]string{"str"}, msg)
So(msg, ShouldResembleProto, &testMsg{})
})
Convey("keep subfield of repeated message field entirely", func() {
msg := &testMsg{
Msgs: []*testMsg{
{Num: 1, Str: "abc"},
{Num: 2, Str: "bcd"},
},
}
testTrim([]string{"msgs"}, msg)
So(msg, ShouldResembleProto, &testMsg{
Msgs: []*testMsg{
{Num: 1, Str: "abc"},
{Num: 2, Str: "bcd"},
},
})
})
Convey("keep subfield of repeated message field partially", func() {
msg := &testMsg{
Msgs: []*testMsg{
{Num: 1, Str: "abc"},
{Num: 2, Str: "bcd"},
},
}
testTrim([]string{"msgs.*.str"}, msg)
So(msg, ShouldResembleProto, &testMsg{
Msgs: []*testMsg{
{Str: "abc"},
{Str: "bcd"},
},
})
})
Convey("trim map field", func() {
msg := &testMsg{
MapStrNum: map[string]int32{"a": 1},
}
testTrim([]string{"str"}, msg)
So(msg, ShouldResembleProto, &testMsg{})
})
Convey("trim map (scalar value)", func() {
msg := &testMsg{
MapStrNum: map[string]int32{
"a": 1,
"b": 2,
},
}
testTrim([]string{"map_str_num.a"}, msg)
So(msg, ShouldResembleProto, &testMsg{
MapStrNum: map[string]int32{
"a": 1,
},
})
})
Convey("trim map (message value) ", func() {
msg := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
}
testTrim([]string{"map_str_msg.a"}, msg)
So(msg, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
},
})
})
Convey("trim map (message value) and keep message value partially", func() {
msg := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1, Str: "abc"},
"b": {Num: 2, Str: "bcd"},
},
}
testTrim([]string{"map_str_msg.a.num"}, msg)
So(msg, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
},
})
})
Convey("trim map (message value) with star key and keep message value partially", func() {
msg := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1, Str: "abc"},
"b": {Num: 2, Str: "bcd"},
},
}
testTrim([]string{"map_str_msg.*.num"}, msg)
So(msg, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
})
})
Convey("trim map (message value) with both star key and actual key", func() {
msg := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1, Str: "abc"},
"b": {Num: 2, Str: "bcd"},
},
}
testTrim([]string{"map_str_msg.*.num", "map_str_msg.a"}, msg)
// expect keep "a" entirely and "b" partially
So(msg, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1, Str: "abc"},
"b": {Num: 2},
},
})
})
Convey("No-op for empty mask trim", func() {
m, msg := Mask{}, &testMsg{Num: 1}
m.Trim(msg)
So(msg, ShouldResembleProto, &testMsg{Num: 1})
})
Convey("Error when trim nil message", func() {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: []string{"str"}}, &testMsg{}, false, false)
So(err, ShouldBeNil)
var msg proto.Message
err = m.Trim(msg)
So(err, ShouldErrLike, "nil message")
})
Convey("Error when descriptor mismatch", func() {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: []string{"str"}}, &testMsg{}, false, false)
So(err, ShouldBeNil)
err = m.Trim(&testingpb.Simple{})
So(err, ShouldErrLike, "expected message have descriptor: internal.testing.Full; got descriptor: internal.testing.Simple")
})
})
}
func TestIncludes(t *testing.T) {
Convey("Test include", t, func() {
testIncludes := func(maskPaths []string, path string, expectedIncl Inclusiveness) {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: maskPaths}, &testMsg{}, false, false)
So(err, ShouldBeNil)
actual, err := m.Includes(path)
So(err, ShouldBeNil)
So(actual, ShouldEqual, expectedIncl)
}
Convey("all", func() {
testIncludes([]string{}, "str", IncludeEntirely)
})
Convey("entirely", func() {
testIncludes([]string{"str"}, "str", IncludeEntirely)
})
Convey("entirely multiple levels", func() {
testIncludes([]string{"msg.msg.str"}, "msg.msg.str", IncludeEntirely)
})
Convey("entirely star", func() {
testIncludes([]string{"map_str_msg.*.str"}, "map_str_msg.xyz.str", IncludeEntirely)
})
Convey("partially", func() {
testIncludes([]string{"msg.str"}, "msg", IncludePartially)
})
Convey("partially multiple levels", func() {
testIncludes([]string{"msg.msg.str"}, "msg.msg", IncludePartially)
})
Convey("partially star", func() {
testIncludes([]string{"map_str_msg.*.str"}, "map_str_msg.xyz", IncludePartially)
})
Convey("exclude", func() {
testIncludes([]string{"str"}, "num", Exclude)
})
Convey("exclude multiple levels", func() {
testIncludes([]string{"msg.str"}, "msg.num", Exclude)
})
})
Convey("Test MustIncludes", t, func() {
testMustIncludes := func(maskPaths []string, path string, expectedIncl Inclusiveness) {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: maskPaths}, &testMsg{}, false, false)
So(err, ShouldBeNil)
actual := m.MustIncludes(path)
So(actual, ShouldEqual, expectedIncl)
}
Convey("works", func() {
testMustIncludes([]string{}, "str", IncludeEntirely)
})
Convey("panics", func() {
// expected delimiter: .; got @'
So(func() { testMustIncludes([]string{}, "str@", IncludeEntirely) }, ShouldPanic)
})
})
}
func TestMerge(t *testing.T) {
Convey("Test merge", t, func() {
testMerge := func(maskPaths []string, src *testMsg, dest *testMsg) {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: maskPaths}, &testMsg{}, false, false)
So(err, ShouldBeNil)
So(m.Merge(src, dest), ShouldBeNil)
}
Convey("scalar field", func() {
src := &testMsg{Num: 1}
dest := &testMsg{Num: 2}
testMerge([]string{"num"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{Num: 1})
})
Convey("repeated scalar field", func() {
src := &testMsg{Nums: []int32{1, 2, 3}}
dest := &testMsg{Nums: []int32{4, 5}}
testMerge([]string{"nums"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{Nums: []int32{1, 2, 3}})
})
Convey("repeated message field", func() {
src := &testMsg{
Msgs: []*testMsg{
{Num: 1},
{Str: "abc"},
},
}
dest := &testMsg{
Msgs: []*testMsg{
{Msg: &testMsg{}},
},
}
testMerge([]string{"msgs"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
Msgs: []*testMsg{
{Num: 1},
{Str: "abc"},
},
})
})
Convey("entire submessage", func() {
src := &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
}
dest := &testMsg{
Msg: &testMsg{
Num: 2,
Str: "def",
},
}
testMerge([]string{"msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
})
})
Convey("partial submessage", func() {
src := &testMsg{
Msg: &testMsg{
Num: 1,
Str: "abc",
},
}
dest := &testMsg{
Msg: &testMsg{
Num: 2,
Str: "def",
},
}
testMerge([]string{"msg.num"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
Msg: &testMsg{
Num: 1,
Str: "def",
},
})
})
Convey("nil message", func() {
src := &testMsg{
Msg: nil,
}
Convey("last seg is message itself", func() {
dest := &testMsg{
Msg: &testMsg{
Str: "def",
},
}
testMerge([]string{"msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{})
})
Convey("last seg is field in message", func() {
dest := &testMsg{
Msg: &testMsg{
Str: "def",
},
}
testMerge([]string{"msg.num"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
Msg: &testMsg{
Str: "def",
},
})
testMerge([]string{"msg.str"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
Msg: &testMsg{},
})
})
Convey("dest is also nil messages", func() {
dest := &testMsg{
Msg: nil,
}
testMerge([]string{"msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{})
testMerge([]string{"msg.str"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{})
})
})
Convey("map field ", func() {
src := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
}
dest := &testMsg{
MapStrMsg: map[string]*testMsg{
"c": {Num: 1},
},
}
testMerge([]string{"map_str_msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
})
})
Convey("map field (dest map is nil)", func() {
src := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
}
dest := &testMsg{
MapStrMsg: nil,
}
testMerge([]string{"map_str_msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": {Num: 1},
"b": {Num: 2},
},
})
})
Convey("map field (value is nil message)", func() {
src := &testMsg{
MapStrMsg: map[string]*testMsg{
"a": nil,
},
}
dest := &testMsg{}
testMerge([]string{"map_str_msg"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{
MapStrMsg: map[string]*testMsg{
"a": nil,
},
})
})
Convey("empty mask", func() {
src := &testMsg{Num: 1}
dest := &testMsg{Num: 2}
m := &Mask{}
So(m.Merge(src, dest), ShouldBeNil)
So(dest, ShouldResembleProto, &testMsg{Num: 2})
})
Convey("multiple fields", func() {
src := &testMsg{Num: 1, Strs: []string{"a", "b"}}
dest := &testMsg{Num: 2, Strs: []string{"c"}}
testMerge([]string{"num", "strs"}, src, dest)
So(dest, ShouldResembleProto, &testMsg{Num: 1, Strs: []string{"a", "b"}})
})
Convey("Error when one of proto message is nil", func() {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: []string{"str"}}, &testMsg{}, false, false)
So(err, ShouldBeNil)
var src proto.Message
So(m.Merge(src, &testMsg{}), ShouldErrLike, "src message: nil message")
})
Convey("Error when proto message descriptors does not match", func() {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: []string{"str"}}, &testMsg{}, false, false)
So(err, ShouldBeNil)
err = m.Merge(&testMsg{}, &testingpb.Simple{})
So(err, ShouldErrLike, "dest message: expected message have descriptor: internal.testing.Full; got descriptor: internal.testing.Simple")
})
})
}
func TestSubmask(t *testing.T) {
buildMask := func(paths ...string) *Mask {
m, err := FromFieldMask(&field_mask.FieldMask{Paths: paths}, &testMsg{}, false, false)
So(err, ShouldBeNil)
return m
}
Convey("Test submask", t, func() {
Convey("when path is partially included", func() {
actual, err := buildMask("msg.msgs.*.str").Submask("msg")
So(err, ShouldBeNil)
assertMaskEqual(actual, buildMask("msgs.*.str"))
})
Convey("when path is entirely included", func() {
actual, err := buildMask("msg").Submask("msg.msgs.*.msg")
So(err, ShouldBeNil)
assertMaskEqual(actual, &Mask{descriptor: testMsgDescriptor})
})
Convey("Error when path is excluded", func() {
_, err := buildMask("msg.msg.str").Submask("str")
So(err, ShouldErrLike, "the given path \"str\" is excluded from mask")
})
Convey("when path ends with star", func() {
actual, err := buildMask("msgs.*.str").Submask("msgs.*")
So(err, ShouldBeNil)
assertMaskEqual(actual, buildMask("str"))
})
})
Convey("Test MustSubmask", t, func() {
m := buildMask("msg.msg.str")
Convey("works", func() {
assertMaskEqual(m.MustSubmask("msg.msg"), buildMask("str"))
})
Convey("panics", func() {
// the given path "str" is excluded from mask
So(func() { m.MustSubmask("str") }, ShouldPanic)
// expected delimiter: .; got @'
So(func() { m.MustSubmask("str@") }, ShouldPanic)
})
})
}
// TODO(yiwzhang): ShouldResemble will hit infinite loop when comparing
// descriptor. Comparing the full name of message as a temporary workaround
func assertMaskEqual(actual *Mask, expect *Mask) {
if expect.descriptor == nil {
So(actual.descriptor, ShouldBeNil)
} else {
So(actual.descriptor, ShouldNotBeNil)
So(actual.descriptor.FullName(), ShouldEqual, expect.descriptor.FullName())
}
So(actual.isRepeated, ShouldEqual, expect.isRepeated)
So(actual.children, ShouldHaveLength, len(expect.children))
for seg, expectSubmask := range expect.children {
So(actual.children, ShouldContainKey, seg)
assertMaskEqual(actual.children[seg], expectSubmask)
}
}