blob: a402b05a07ca1bd86280a2b35de12cfb457cae1c [file] [log] [blame]
// Copyright 2022 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 protowalk
import (
"fmt"
"sort"
"testing"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
)
type CustomChecker struct{}
var _ FieldProcessor = (*CustomChecker)(nil)
func (*CustomChecker) Process(field protoreflect.FieldDescriptor, msg protoreflect.Message) (data ResultData, applied bool) {
chk := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), E_Custom).(*CustomExt)
s := msg.Get(field).String()
if applied = s != chk.MustEqual; applied {
data = ResultData{Message: fmt.Sprintf("%q doesn't equal %q", s, chk.MustEqual)}
}
return
}
func init() {
RegisterFieldProcessor(&CustomChecker{}, func(field protoreflect.FieldDescriptor) ProcessAttr {
if fo := field.Options().(*descriptorpb.FieldOptions); fo != nil {
if proto.GetExtension(fo, E_Custom).(*CustomExt) != nil {
return ProcessAlways
}
}
return ProcessNever
})
}
func TestFields(t *testing.T) {
// These tests interact with globalFieldProcessorCache
// Hence, no t.Parallel()
ftt.Run(`Fields`, t, func(t *ftt.Test) {
// Reset the cache
resetGlobalFieldProcessorCache()
t.Run(`works with no FieldProcessors`, func(t *ftt.Test) {
msg := &Outer{}
assert.Loosely(t, Fields(msg), should.BeEmpty)
assert.Loosely(t, globalFieldProcessorCache, should.BeEmpty)
})
t.Run(`works with one processor on empty message`, func(t *ftt.Test) {
msg := &Outer{}
assert.Loosely(t, Fields(msg, &DeprecatedProcessor{}), should.Resemble(Results{nil}))
keys := make([]string, 0, len(globalFieldProcessorCache))
for k := range globalFieldProcessorCache {
keys = append(keys, fmt.Sprintf("%s+%s", k.message, k.processorT))
}
sort.Strings(keys)
assert.Loosely(t, keys, should.Resemble([]string{
"protowalk.Inner+*protowalk.DeprecatedProcessor",
"protowalk.Inner.Embedded+*protowalk.DeprecatedProcessor",
"protowalk.Outer+*protowalk.DeprecatedProcessor",
}))
})
t.Run(`works on populated messages`, func(t *ftt.Test) {
msg := &Outer{Deprecated: "extra"}
assert.Loosely(t, Fields(msg, &DeprecatedProcessor{}).Strings(), should.Resemble([]string{
".deprecated: deprecated",
}))
})
t.Run(`works on nested populated messages`, func(t *ftt.Test) {
msg := &Outer{SingleInner: &Inner{Deprecated: "extra"}}
assert.Loosely(t, Fields(msg, &DeprecatedProcessor{}).Strings(), should.Resemble([]string{
".single_inner.deprecated: deprecated",
}))
})
t.Run(`works on maps`, func(t *ftt.Test) {
msg := &Outer{
MapInner: map[string]*Inner{
"something": {Deprecated: "yo"},
},
IntMapInner: map[int32]*Inner{
20: {Deprecated: "hay"},
},
}
assert.Loosely(t, Fields(msg, &DeprecatedProcessor{}).Strings(), should.Resemble([]string{
".map_inner[\"something\"].deprecated: deprecated",
".int_map_inner[20].deprecated: deprecated",
}))
})
t.Run(`works on lists`, func(t *ftt.Test) {
msg := &Outer{
MultiInner: []*Inner{
{},
{},
{Deprecated: "hay"},
},
}
assert.Loosely(t, Fields(msg, &DeprecatedProcessor{}).Strings(), should.Resemble([]string{
".multi_inner[2].deprecated: deprecated",
}))
})
t.Run(`works with custom check`, func(t *ftt.Test) {
msg := &Outer{
MultiInner: []*Inner{
{Custom: "neat"},
{},
{Custom: "hello"},
{},
},
}
assert.Loosely(t, Fields(msg, &CustomChecker{}).Strings(), should.Resemble([]string{
`.custom: "" doesn't equal "hello"`,
`.multi_inner[0].custom: "neat" doesn't equal "hello"`,
`.multi_inner[1].custom: "" doesn't equal "hello"`,
// 2 is OK!
`.multi_inner[3].custom: "" doesn't equal "hello"`,
}))
})
})
}
func BenchmarkCache(b *testing.B) {
b.ReportAllocs()
desc := (&Outer{}).ProtoReflect().Descriptor()
checker := &CustomChecker{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
generateCacheEntry(desc, lookupProcBundles(checker)[0], stringset.New(0), map[string]*cacheEntryBuilder{})
}
}
func BenchmarkFields(b *testing.B) {
b.ReportAllocs()
msg := &Outer{
Deprecated: "hey",
SingleInner: &Inner{
Regular: "things",
Deprecated: "yo",
},
MapInner: map[string]*Inner{
"schwoot": {
Deprecated: "thing",
SingleEmbed: &Inner_Embedded{
Deprecated: "yarp",
},
MultiEmbed: []*Inner_Embedded{
{Deprecated: "yay"},
{Regular: "ignore"},
},
},
"nerps": {
Deprecated: "thing",
SingleEmbed: &Inner_Embedded{},
MultiEmbed: []*Inner_Embedded{
{},
},
},
},
MultiDeprecated: []*Inner{
{Regular: "something"},
{Deprecated: "something else"},
},
MultiInner: []*Inner{
{Custom: "neat"},
{},
{Custom: "hello"},
{},
},
}
checkers := []FieldProcessor{
&DeprecatedProcessor{},
&RequiredProcessor{},
&CustomChecker{},
}
for _, chk := range checkers {
setCacheEntry(msg.ProtoReflect().Descriptor(), lookupProcBundles(chk)[0], stringset.New(0), map[string]*cacheEntryBuilder{})
}
expect := []string{
`.deprecated: deprecated`,
`.single_inner.deprecated: deprecated`,
`.map_inner["nerps"].deprecated: deprecated`,
`.map_inner["schwoot"].deprecated: deprecated`,
`.map_inner["schwoot"].single_embed.deprecated: deprecated`,
`.map_inner["schwoot"].multi_embed[0].deprecated: deprecated`,
`.multi_deprecated: deprecated`,
`.multi_deprecated[1].deprecated: deprecated`,
`.req: required`,
`.single_inner.req: required`,
`.multi_inner[0].req: required`,
`.multi_inner[1].req: required`,
`.multi_inner[2].req: required`,
`.multi_inner[3].req: required`,
`.map_inner["nerps"].req: required`,
`.map_inner["nerps"].single_embed.req: required`,
`.map_inner["nerps"].multi_embed[0].req: required`,
`.map_inner["schwoot"].req: required`,
`.map_inner["schwoot"].single_embed.req: required`,
`.map_inner["schwoot"].multi_embed[0].req: required`,
`.map_inner["schwoot"].multi_embed[1].req: required`,
`.multi_deprecated[0].req: required`,
`.multi_deprecated[1].req: required`,
`.custom: "" doesn't equal "hello"`,
`.single_inner.custom: "" doesn't equal "hello"`,
`.multi_inner[0].custom: "neat" doesn't equal "hello"`,
`.multi_inner[1].custom: "" doesn't equal "hello"`,
`.multi_inner[3].custom: "" doesn't equal "hello"`,
`.map_inner["nerps"].custom: "" doesn't equal "hello"`,
`.map_inner["nerps"].single_embed.custom: "" doesn't equal "hello"`,
`.map_inner["nerps"].multi_embed[0].custom: "" doesn't equal "hello"`,
`.map_inner["schwoot"].custom: "" doesn't equal "hello"`,
`.map_inner["schwoot"].single_embed.custom: "" doesn't equal "hello"`,
`.map_inner["schwoot"].multi_embed[0].custom: "" doesn't equal "hello"`,
`.map_inner["schwoot"].multi_embed[1].custom: "" doesn't equal "hello"`,
`.multi_deprecated[0].custom: "" doesn't equal "hello"`,
`.multi_deprecated[1].custom: "" doesn't equal "hello"`,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
reports := Fields(msg, checkers...)
b.StopTimer()
for ridx, report := range reports.Strings() {
if report != expect[ridx] {
b.Errorf("iter[%d]: report[%d]: %q != %q", i, ridx, report, expect[ridx])
}
}
b.StartTimer()
}
}