Created
March 10, 2023 20:45
-
-
Save shoenig/d90586a75c7aec55e1a26d09723a95e0 to your computer and use it in GitHub Desktop.
Better Go opaque map comparison
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
) | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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