diff --git a/Makefile b/Makefile index 1d7fd6a..1dc5f7f 100644 --- a/Makefile +++ b/Makefile @@ -47,9 +47,9 @@ GO_IMPORT:=$(subst $(space),,$(GO_IMPORT_SPACES)) PHONE: generated-code generated-code: PATH=$(DEPSGOBIN):$$PATH protoc -I=. --go_out="${EXT_IMPORT}:." extproto/ext.proto - PATH=$(DEPSGOBIN):$$PATH cp -r ${PACKAGE}/extproto/ extproto - PATH=$(DEPSGOBIN):$$PATH protoc -I=. -I=./extproto --go_out="." --ext_out="." tests/api/hello.proto - PATH=$(DEPSGOBIN):$$PATH cp -r ${PACKAGE}/tests/api/ tests/api + PATH=$(DEPSGOBIN):$$PATH cp -r ${PACKAGE}/extproto/* extproto + PATH=$(DEPSGOBIN):$$PATH protoc -I=. -I=./extproto --go_out="." --ext_out="." tests/api/hello.proto + PATH=$(DEPSGOBIN):$$PATH cp -r ${PACKAGE}/tests/api/* tests/api/ PATH=$(DEPSGOBIN):$$PATH rm -rf github.com PATH=$(DEPSGOBIN):$$PATH goimports -w . diff --git a/go.mod b/go.mod index 87b07dd..2f39ea3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/golang/protobuf v1.5.2 github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 github.com/lyft/protoc-gen-star v0.6.0 - github.com/mitchellh/hashstructure v1.0.0 github.com/onsi/ginkgo v1.10.3 github.com/onsi/gomega v1.7.1 golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect diff --git a/go.sum b/go.sum index 1c03ae1..90b1a9b 100644 --- a/go.sum +++ b/go.sum @@ -3,33 +3,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/lyft/protoc-gen-star v0.5.1 h1:sImehRT+p7lW9n6R7MQc5hVgzWGEkDVZU4AsBQ4Isu8= -github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= github.com/lyft/protoc-gen-star v0.6.0 h1:xOpFu4vwmIoUeUrRuAtdCrZZymT/6AkW/bsUWA506Fo= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= -github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -61,18 +46,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/pkg/hasher/hashstructure/hashstructure.go b/pkg/hasher/hashstructure/hashstructure.go new file mode 100644 index 0000000..8d4e8ae --- /dev/null +++ b/pkg/hasher/hashstructure/hashstructure.go @@ -0,0 +1,335 @@ +/** +This is a slightly modified version of github.com/mitchellh/hashstructure@v1.0.0 + +The reduced feature set is to avoid using reflect.Value.Interface() that can cause data races when +used with google.golang.org/protobuf. + +With the new `google.golang.org/protobuf` package, functions like Clone and Marshal can mutate +private message state; When hash is performed in a different go-routine, `reflect.Value.Interface()` +seem to read this state, triggering a data race when code is run in the race detector. + +The original hash structure called `reflect.Value.Interface()` on structs to see if they implement +hashstructure specific interfaces. As proto messages are generated code, we know that they do not implement +these interfaces, so we can safely drop the support for these interfaces and remove the call to +`reflect.Value.Interface()` - thus solving the data race. +*/ + +package hashstructure + +import ( + "encoding/binary" + "fmt" + "hash" + "hash/fnv" + "reflect" +) + +// ErrNotStringer is returned when there's an error with hash:"string" +type ErrNotStringer struct { + Field string +} + +// Error implements error for ErrNotStringer +func (ens *ErrNotStringer) Error() string { + return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) +} + +// HashOptions are options that are available for hashing. +type HashOptions struct { + // Hasher is the hash function to use. If this isn't set, it will + // default to FNV. + Hasher hash.Hash64 + + // TagName is the struct tag to look at when hashing the structure. + // By default this is "hash". + TagName string + + // ZeroNil is flag determining if nil pointer should be treated equal + // to a zero value of pointed type. By default this is false. + ZeroNil bool +} + +// Hash returns the hash value of an arbitrary value. +// +// If opts is nil, then default options will be used. See HashOptions +// for the default values. The same *HashOptions value cannot be used +// concurrently. None of the values within a *HashOptions struct are +// safe to read/write while hashing is being done. +// +// Notes on the value: +// +// - Unexported fields on structs are ignored and do not affect the +// hash value. +// +// - Adding an exported field to a struct with the zero value will change +// the hash value. +// +// For structs, the hashing can be controlled using tags. For example: +// +// struct { +// Name string +// UUID string `hash:"ignore"` +// } +// +// The available tag values are: +// +// - "ignore" or "-" - The field will be ignored and not affect the hash code. +// +// - "set" - The field will be treated as a set, where ordering doesn't +// affect the hash code. This only works for slices. +// +// - "string" - The field will be hashed as a string, only works when the +// field implements fmt.Stringer +func Hash(v interface{}, opts *HashOptions) (uint64, error) { + // Create default options + if opts == nil { + opts = &HashOptions{} + } + if opts.Hasher == nil { + opts.Hasher = fnv.New64() + } + if opts.TagName == "" { + opts.TagName = "hash" + } + + // Reset the hash + opts.Hasher.Reset() + + // Create our walker and walk the structure + w := &walker{ + h: opts.Hasher, + tag: opts.TagName, + zeronil: opts.ZeroNil, + } + return w.visit(reflect.ValueOf(v), nil) +} + +type walker struct { + h hash.Hash64 + tag string + zeronil bool +} + +type visitOpts struct { + // Flags are a bitmask of flags to affect behavior of this visit + Flags visitFlag +} + +func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { + t := reflect.TypeOf(0) + + // Loop since these can be wrapped in multiple layers of pointers + // and interfaces. + for { + // If we have an interface, dereference it. We have to do this up + // here because it might be a nil in there and the check below must + // catch that. + if v.Kind() == reflect.Interface { + v = v.Elem() + continue + } + + if v.Kind() == reflect.Ptr { + if w.zeronil { + t = v.Type().Elem() + } + v = reflect.Indirect(v) + continue + } + + break + } + + // If it is nil, treat it like a zero. + if !v.IsValid() { + v = reflect.Zero(t) + } + + // Binary writing can use raw ints, we have to convert to + // a sized-int, we'll choose the largest... + switch v.Kind() { + case reflect.Int: + v = reflect.ValueOf(int64(v.Int())) + case reflect.Uint: + v = reflect.ValueOf(uint64(v.Uint())) + case reflect.Bool: + var tmp int8 + if v.Bool() { + tmp = 1 + } + v = reflect.ValueOf(tmp) + } + + k := v.Kind() + + // We can shortcut numeric values by directly binary writing them + if k >= reflect.Int && k <= reflect.Complex64 { + // A direct hash calculation + w.h.Reset() + err := binary.Write(w.h, binary.LittleEndian, v.Interface()) + return w.h.Sum64(), err + } + + switch k { + case reflect.Array: + var h uint64 + l := v.Len() + for i := 0; i < l; i++ { + current, err := w.visit(v.Index(i), nil) + if err != nil { + return 0, err + } + + h = hashUpdateOrdered(w.h, h, current) + } + + return h, nil + + case reflect.Map: + + // Build the hash for the map. We do this by XOR-ing all the key + // and value hashes. This makes it deterministic despite ordering. + var h uint64 + for _, k := range v.MapKeys() { + v := v.MapIndex(k) + + kh, err := w.visit(k, nil) + if err != nil { + return 0, err + } + vh, err := w.visit(v, nil) + if err != nil { + return 0, err + } + + fieldHash := hashUpdateOrdered(w.h, kh, vh) + h = hashUpdateUnordered(h, fieldHash) + } + + return h, nil + + case reflect.Struct: + + t := v.Type() + h, err := w.visit(reflect.ValueOf(t.Name()), nil) + if err != nil { + return 0, err + } + + l := v.NumField() + for i := 0; i < l; i++ { + if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { + var f visitFlag + fieldType := t.Field(i) + if fieldType.PkgPath != "" { + // Unexported + continue + } + + tag := fieldType.Tag.Get(w.tag) + if tag == "ignore" || tag == "-" { + // Ignore this field + continue + } + + // if string is set, use the string value + if tag == "string" { + if impl, ok := innerV.Interface().(fmt.Stringer); ok { + innerV = reflect.ValueOf(impl.String()) + } else { + return 0, &ErrNotStringer{ + Field: v.Type().Field(i).Name, + } + } + } + + switch tag { + case "set": + f |= visitFlagSet + } + + kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) + if err != nil { + return 0, err + } + + vh, err := w.visit(innerV, &visitOpts{ + Flags: f, + }) + if err != nil { + return 0, err + } + + fieldHash := hashUpdateOrdered(w.h, kh, vh) + h = hashUpdateUnordered(h, fieldHash) + } + } + + return h, nil + + case reflect.Slice: + // We have two behaviors here. If it isn't a set, then we just + // visit all the elements. If it is a set, then we do a deterministic + // hash code. + var h uint64 + var set bool + if opts != nil { + set = (opts.Flags & visitFlagSet) != 0 + } + l := v.Len() + for i := 0; i < l; i++ { + current, err := w.visit(v.Index(i), nil) + if err != nil { + return 0, err + } + + if set { + h = hashUpdateUnordered(h, current) + } else { + h = hashUpdateOrdered(w.h, h, current) + } + } + + return h, nil + + case reflect.String: + // Directly hash + w.h.Reset() + _, err := w.h.Write([]byte(v.String())) + return w.h.Sum64(), err + + default: + return 0, fmt.Errorf("unknown kind to hash: %s", k) + } + +} + +func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { + // For ordered updates, use a real hash function + h.Reset() + + // We just panic if the binary writes fail because we are writing + // an int64 which should never be fail-able. + e1 := binary.Write(h, binary.LittleEndian, a) + e2 := binary.Write(h, binary.LittleEndian, b) + if e1 != nil { + panic(e1) + } + if e2 != nil { + panic(e2) + } + + return h.Sum64() +} + +func hashUpdateUnordered(a, b uint64) uint64 { + return a ^ b +} + +// visitFlag is used as a bitmask for affecting visit behavior +type visitFlag uint + +const ( + visitFlagInvalid visitFlag = iota + visitFlagSet = iota << 1 +) diff --git a/pkg/hasher/hashstructure/hashstructure_test.go b/pkg/hasher/hashstructure/hashstructure_test.go new file mode 100644 index 0000000..1e3e437 --- /dev/null +++ b/pkg/hasher/hashstructure/hashstructure_test.go @@ -0,0 +1,477 @@ +/* +This is a slightly modified version of github.com/mitchellh/hashstructure@v1.0.0 +See comment in hashstructure.go for more details. +*/ +package hashstructure + +import ( + "fmt" + "testing" + "time" +) + +func TestHash_identity(t *testing.T) { + cases := []interface{}{ + nil, + "foo", + 42, + true, + false, + []string{"foo", "bar"}, + []interface{}{1, nil, "foo"}, + map[string]string{"foo": "bar"}, + map[interface{}]string{"foo": "bar"}, + map[interface{}]interface{}{"foo": "bar", "bar": 0}, + struct { + Foo string + Bar []interface{} + }{ + Foo: "foo", + Bar: []interface{}{nil, nil, nil}, + }, + &struct { + Foo string + Bar []interface{} + }{ + Foo: "foo", + Bar: []interface{}{nil, nil, nil}, + }, + } + + for _, tc := range cases { + // We run the test 100 times to try to tease out variability + // in the runtime in terms of ordering. + valuelist := make([]uint64, 100) + for i, _ := range valuelist { + v, err := Hash(tc, nil) + if err != nil { + t.Fatalf("Error: %s\n\n%#v", err, tc) + } + + valuelist[i] = v + } + + // Zero is always wrong + if valuelist[0] == 0 { + t.Fatalf("zero hash: %#v", tc) + } + + // Make sure all the values match + t.Logf("%#v: %d", tc, valuelist[0]) + for i := 1; i < len(valuelist); i++ { + if valuelist[i] != valuelist[0] { + t.Fatalf("non-matching: %d, %d\n\n%#v", i, 0, tc) + } + } + } +} + +func TestHash_equal(t *testing.T) { + type testFoo struct{ Name string } + type testBar struct{ Name string } + + cases := []struct { + One, Two interface{} + Match bool + }{ + { + map[string]string{"foo": "bar"}, + map[interface{}]string{"foo": "bar"}, + true, + }, + + { + map[string]interface{}{"1": "1"}, + map[string]interface{}{"1": "1", "2": "2"}, + false, + }, + + { + struct{ Fname, Lname string }{"foo", "bar"}, + struct{ Fname, Lname string }{"bar", "foo"}, + false, + }, + + { + struct{ Lname, Fname string }{"foo", "bar"}, + struct{ Fname, Lname string }{"foo", "bar"}, + false, + }, + + { + struct{ Lname, Fname string }{"foo", "bar"}, + struct{ Fname, Lname string }{"bar", "foo"}, + true, + }, + + { + testFoo{"foo"}, + testBar{"foo"}, + false, + }, + + { + struct { + Foo string + unexported string + }{ + Foo: "bar", + unexported: "baz", + }, + struct { + Foo string + unexported string + }{ + Foo: "bar", + unexported: "bang", + }, + true, + }, + + { + struct { + testFoo + Foo string + }{ + Foo: "bar", + testFoo: testFoo{Name: "baz"}, + }, + struct { + testFoo + Foo string + }{ + Foo: "bar", + }, + true, + }, + + { + struct { + Foo string + }{ + Foo: "bar", + }, + struct { + testFoo + Foo string + }{ + Foo: "bar", + }, + true, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Logf("Hashing: %#v", tc.One) + one, err := Hash(tc.One, nil) + t.Logf("Result: %d", one) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + t.Logf("Hashing: %#v", tc.Two) + two, err := Hash(tc.Two, nil) + t.Logf("Result: %d", two) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.Two, err) + } + + // Zero is always wrong + if one == 0 { + t.Fatalf("zero hash: %#v", tc.One) + } + + // Compare + if (one == two) != tc.Match { + t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) + } + }) + } +} + +func TestHash_equalIgnore(t *testing.T) { + type Test1 struct { + Name string + UUID string `hash:"ignore"` + } + + type Test2 struct { + Name string + UUID string `hash:"-"` + } + + type TestTime struct { + Name string + Time time.Time `hash:"string"` + } + + type TestTime2 struct { + Name string + Time time.Time + } + + now := time.Now() + cases := []struct { + One, Two interface{} + Match bool + }{ + { + Test1{Name: "foo", UUID: "foo"}, + Test1{Name: "foo", UUID: "bar"}, + true, + }, + + { + Test1{Name: "foo", UUID: "foo"}, + Test1{Name: "foo", UUID: "foo"}, + true, + }, + + { + Test2{Name: "foo", UUID: "foo"}, + Test2{Name: "foo", UUID: "bar"}, + true, + }, + + { + Test2{Name: "foo", UUID: "foo"}, + Test2{Name: "foo", UUID: "foo"}, + true, + }, + { + TestTime{Name: "foo", Time: now}, + TestTime{Name: "foo", Time: time.Time{}}, + false, + }, + { + TestTime{Name: "foo", Time: now}, + TestTime{Name: "foo", Time: now}, + true, + }, + { + TestTime2{Name: "foo", Time: now}, + TestTime2{Name: "foo", Time: time.Time{}}, + true, + }, + } + + for _, tc := range cases { + one, err := Hash(tc.One, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + two, err := Hash(tc.Two, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.Two, err) + } + + // Zero is always wrong + if one == 0 { + t.Fatalf("zero hash: %#v", tc.One) + } + + // Compare + if (one == two) != tc.Match { + t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) + } + } +} + +func TestHash_stringTagError(t *testing.T) { + type Test1 struct { + Name string + BrokenField string `hash:"string"` + } + + type Test2 struct { + Name string + BustedField int `hash:"string"` + } + + type Test3 struct { + Name string + Time time.Time `hash:"string"` + } + + cases := []struct { + Test interface{} + Field string + }{ + { + Test1{Name: "foo", BrokenField: "bar"}, + "BrokenField", + }, + { + Test2{Name: "foo", BustedField: 23}, + "BustedField", + }, + { + Test3{Name: "foo", Time: time.Now()}, + "", + }, + } + + for _, tc := range cases { + _, err := Hash(tc.Test, nil) + if err != nil { + if ens, ok := err.(*ErrNotStringer); ok { + if ens.Field != tc.Field { + t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field) + } + } else { + t.Fatalf("unknown error %#v: got %s", tc, err) + } + } + } +} + +func TestHash_equalNil(t *testing.T) { + type Test struct { + Str *string + Int *int + Map map[string]string + Slice []string + } + + cases := []struct { + One, Two interface{} + ZeroNil bool + Match bool + }{ + { + Test{ + Str: nil, + Int: nil, + Map: nil, + Slice: nil, + }, + Test{ + Str: new(string), + Int: new(int), + Map: make(map[string]string), + Slice: make([]string, 0), + }, + true, + true, + }, + { + Test{ + Str: nil, + Int: nil, + Map: nil, + Slice: nil, + }, + Test{ + Str: new(string), + Int: new(int), + Map: make(map[string]string), + Slice: make([]string, 0), + }, + false, + false, + }, + { + nil, + 0, + true, + true, + }, + { + nil, + 0, + false, + true, + }, + } + + for _, tc := range cases { + one, err := Hash(tc.One, &HashOptions{ZeroNil: tc.ZeroNil}) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + two, err := Hash(tc.Two, &HashOptions{ZeroNil: tc.ZeroNil}) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.Two, err) + } + + // Zero is always wrong + if one == 0 { + t.Fatalf("zero hash: %#v", tc.One) + } + + // Compare + if (one == two) != tc.Match { + t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) + } + } +} + +func TestHash_equalSet(t *testing.T) { + type Test struct { + Name string + Friends []string `hash:"set"` + } + + cases := []struct { + One, Two interface{} + Match bool + }{ + { + Test{Name: "foo", Friends: []string{"foo", "bar"}}, + Test{Name: "foo", Friends: []string{"bar", "foo"}}, + true, + }, + + { + Test{Name: "foo", Friends: []string{"foo", "bar"}}, + Test{Name: "foo", Friends: []string{"foo", "bar"}}, + true, + }, + } + + for _, tc := range cases { + one, err := Hash(tc.One, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + two, err := Hash(tc.Two, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.Two, err) + } + + // Zero is always wrong + if one == 0 { + t.Fatalf("zero hash: %#v", tc.One) + } + + // Compare + if (one == two) != tc.Match { + t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) + } + } +} + +type testIncludable struct { + Value string + Ignore string +} + +func (t testIncludable) HashInclude(field string, v interface{}) (bool, error) { + return field != "Ignore", nil +} + +type testIncludableMap struct { + Map map[string]string +} + +func (t testIncludableMap) HashIncludeMap(field string, k, v interface{}) (bool, error) { + if field != "Map" { + return true, nil + } + + if s, ok := k.(string); ok && s == "ignore" { + return false, nil + } + + return true, nil +} diff --git a/templates/hash/file.go b/templates/hash/file.go index 49af779..508e046 100644 --- a/templates/hash/file.go +++ b/templates/hash/file.go @@ -12,7 +12,7 @@ import ( "hash" "hash/fnv" - "github.com/mitchellh/hashstructure" + "github.com/solo-io/protoc-gen-ext/pkg/hasher/hashstructure" safe_hasher "github.com/solo-io/protoc-gen-ext/pkg/hasher" {{ range $path, $pkg := enumPackages (externalEnums .) }} diff --git a/tests/api/hello.pb.go b/tests/api/hello.pb.go index 15693cb..79f3fc2 100644 --- a/tests/api/hello.pb.go +++ b/tests/api/hello.pb.go @@ -262,6 +262,7 @@ type Nested struct { Initial map[string]*Simple `protobuf:"bytes,9,rep,name=initial,proto3" json:"initial,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` SimpleMap map[string]string `protobuf:"bytes,10,rep,name=simple_map,json=simpleMap,proto3" json:"simple_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Types that are assignable to TestOneOf: + // // *Nested_EmptyOneOf // *Nested_NestedOneOf // *Nested_PrimitiveOneOf diff --git a/tests/api/hello.pb.hash.go b/tests/api/hello.pb.hash.go index 5f085aa..fef3d96 100644 --- a/tests/api/hello.pb.hash.go +++ b/tests/api/hello.pb.hash.go @@ -10,8 +10,8 @@ import ( "hash" "hash/fnv" - "github.com/mitchellh/hashstructure" safe_hasher "github.com/solo-io/protoc-gen-ext/pkg/hasher" + "github.com/solo-io/protoc-gen-ext/pkg/hasher/hashstructure" ) // ensure the imports are used