Skip to content

Instantly share code, notes, and snippets.

@shoenig
Created March 10, 2023 20:45
Show Gist options
  • Save shoenig/d90586a75c7aec55e1a26d09723a95e0 to your computer and use it in GitHub Desktop.
Save shoenig/d90586a75c7aec55e1a26d09723a95e0 to your computer and use it in GitHub Desktop.
Better Go opaque map comparison
// net yet used because
// 1. very slow (like 10x worse than DeepEqual)
// 2. terrifying - i mean what even is this
var (
cmpOptIgnorePrivate = ignoreUnexportedAlways()
cmpOptNilIsEmpty = cmpopts.EquateEmpty()
)
// ignoreUnexportedAlways is a derivative of go-cmp.IgnoreUnexported, but this one
// will always ignore unexported types, recursively.
func ignoreUnexportedAlways() cmp.Option {
return cmp.FilterPath(
func(p cmp.Path) bool {
sf, ok := p.Index(-1).(cmp.StructField)
if !ok {
return false
}
r, _ := utf8.DecodeRuneInString(sf.Name())
return !unicode.IsUpper(r)
},
cmp.Ignore(),
)
}
// OpaqueMapsEqual compare maps[<comparable>]<any> for equality, but safely by
// using the cmp package and ignoring un-exported types, and by treating nil/empty
// slices and maps as equal.
func OpaqueMapsEqual[M ~map[K]V, K comparable, V any](m1, m2 M) bool {
return maps.EqualFunc(m1, m2, func(a, b V) bool {
return cmp.Equal(a, b,
cmpOptIgnorePrivate, // ignore all private fields
cmpOptNilIsEmpty, // nil/empty slices treated as equal
)
})
}
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
)
func Test_OpaqueMapsEqual(t *testing.T) {
ci.Parallel(t)
type public struct {
A int
}
type private struct {
a int
}
type mix struct {
A int
b int
}
cases := []struct {
name string
a, b map[string]any
exp bool
}{{
name: "both nil",
a: nil,
b: nil,
exp: true,
}, {
name: "empty and nil",
a: nil,
b: make(map[string]any),
exp: true,
}, {
name: "same strings",
a: map[string]any{"a": "A"},
b: map[string]any{"a": "A"},
exp: true,
}, {
name: "same public struct",
a: map[string]any{"a": &public{A: 42}},
b: map[string]any{"a": &public{A: 42}},
exp: true,
}, {
name: "different public struct",
a: map[string]any{"a": &public{A: 42}},
b: map[string]any{"a": &public{A: 10}},
exp: false,
}, {
name: "different private struct",
a: map[string]any{"a": &private{a: 42}},
b: map[string]any{"a": &private{a: 10}},
exp: true, // private fields not compared
}, {
name: "mix same public different private",
a: map[string]any{"a": &mix{A: 42, b: 1}},
b: map[string]any{"a": &mix{A: 42, b: 2}},
exp: true, // private fields not compared
}, {
name: "mix different public same private",
a: map[string]any{"a": &mix{A: 42, b: 1}},
b: map[string]any{"a": &mix{A: 10, b: 1}},
exp: false,
}, {
name: "nil empty slice values",
a: map[string]any{"a": []string(nil)},
b: map[string]any{"a": make([]string, 0)},
exp: true,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := OpaqueMapsEqual(tc.a, tc.b)
must.Eq(t, tc.exp, result, must.Sprintf("%#v vs %#v", tc.a, tc.b))
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment