diff --git a/proto/pbservice/convert.go b/proto/pbservice/convert.go index c35580d20e..be3de34d39 100644 --- a/proto/pbservice/convert.go +++ b/proto/pbservice/convert.go @@ -74,7 +74,7 @@ func NewCheckServiceNodeFromStructs(t *structs.CheckServiceNode) *CheckServiceNo r := NewNodeServiceFromStructs(*t.Service) s.Service = &r } - t.Checks = make(structs.HealthChecks, len(t.Checks)) + s.Checks = make([]*HealthCheck, len(t.Checks)) for i, c := range t.Checks { if c == nil { continue diff --git a/proto/pbservice/convert_test.go b/proto/pbservice/convert_test.go new file mode 100644 index 0000000000..ba4bc08bb5 --- /dev/null +++ b/proto/pbservice/convert_test.go @@ -0,0 +1,109 @@ +package pbservice + +import ( + "os" + "reflect" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + fuzz "github.com/google/gofuzz" + "github.com/hashicorp/consul/agent/structs" +) + +func TestNewCheckServiceNodeFromStructs_RoundTrip(t *testing.T) { + repeat(t, func(t *testing.T, fuzzer *fuzz.Fuzzer) { + fuzzer.Funcs(randInt32, randUint32, randInterface, randStructsUpstream) + var target structs.CheckServiceNode + fuzzer.Fuzz(&target) + + result := CheckServiceNodeToStructs(NewCheckServiceNodeFromStructs(&target)) + assertEqual(t, &target, result) + }) +} + +func repeat(t *testing.T, fn func(t *testing.T, fuzzer *fuzz.Fuzzer)) { + reps := getEnvIntWithDefault(t, "TEST_REPEAT_COUNT", 5) + seed := getEnvIntWithDefault(t, "TEST_RANDOM_SEED", time.Now().UnixNano()) + t.Logf("using seed %d for %d repetitions", seed, reps) + + fuzzer := fuzz.NewWithSeed(seed) + for i := 0; i < int(reps); i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + fn(t, fuzzer) + }) + } +} + +func getEnvIntWithDefault(t *testing.T, key string, d int64) int64 { + t.Helper() + raw, ok := os.LookupEnv(key) + if !ok { + return d + } + v, err := strconv.Atoi(raw) + if err != nil { + t.Fatalf("invald value for %v: %v", key, err.Error()) + } + return int64(v) +} + +func assertEqual(t *testing.T, x, y interface{}) { + t.Helper() + if diff := cmp.Diff(x, y, cmpopts.EquateEmpty()); diff != "" { + t.Fatalf("assertion failed: values are not equal\n--- original\n+++ result\n\n%v", diff) + } +} + +// randUint32 is a custom fuzzer function which limits all uints to 32 bits. +// This is necessary because the structs types use un-sized uints, however in +// practice they are constrained to 32 bits, and the protobuf types use (u)int32. +// The structs types use (u)int64 for any fields that require 64 bits. +func randUint32(i *uint, c fuzz.Continue) { + *i = uint(c.Rand.Uint32()) +} + +// see randUint32 +func randInt32(i *int, c fuzz.Continue) { + *i = int(c.Rand.Int31()) +} + +// randStructsUpstream is a custom fuzzer function which skips generating values +// for fields enumerated in the ignore-fields annotation. +func randStructsUpstream(u *structs.Upstream, c fuzz.Continue) { + v := reflect.ValueOf(u).Elem() + for i := 0; i < v.NumField(); i++ { + switch v.Type().Field(i).Name { + case "IngressHosts": + continue + } + c.Fuzz(v.Field(i).Addr().Interface()) + } +} + +// randInterface is a custom fuzzer function which generates random data for +// interface{} (most likely used in a map[string]interface{}). +// The random data does not contain any ints (or float32) because protobuf +// converts them to float64, which will cause the test to fail. +func randInterface(m *interface{}, c fuzz.Continue) { + switch c.Rand.Intn(6) { + case 0: + *m = nil + case 1: + *m = c.RandBool() + case 2: + *m = c.Rand.Float64() + case 3: + *m = c.RandString() + case 4: + *m = []interface{}{c.RandString(), c.RandBool(), nil, c.Rand.Float64()} + case 5: + *m = map[string]interface{}{ + c.RandString(): c.RandString(), + c.RandString(): c.Rand.Float64(), + c.RandString(): nil, + } + } +}