From 18255f411ecbacc417df612265f655ba06c0e75f Mon Sep 17 00:00:00 2001 From: Eugene Kim Date: Wed, 24 Apr 2019 11:28:24 -0700 Subject: [PATCH] Add gomock matcher utilities --- gomock_matchers/path.go | 35 ++++++++++ gomock_matchers/path_test.go | 51 +++++++++++++++ gomock_matchers/slice.go | 39 +++++++++++ gomock_matchers/slice_test.go | 114 +++++++++++++++++++++++++++++++++ gomock_matchers/struct.go | 53 +++++++++++++++ gomock_matchers/struct_test.go | 113 ++++++++++++++++++++++++++++++++ gomock_matchers/util.go | 12 ++++ 7 files changed, 417 insertions(+) create mode 100644 gomock_matchers/path.go create mode 100644 gomock_matchers/path_test.go create mode 100644 gomock_matchers/slice.go create mode 100644 gomock_matchers/slice_test.go create mode 100644 gomock_matchers/struct.go create mode 100644 gomock_matchers/struct_test.go create mode 100644 gomock_matchers/util.go diff --git a/gomock_matchers/path.go b/gomock_matchers/path.go new file mode 100644 index 000000000..62b3510f7 --- /dev/null +++ b/gomock_matchers/path.go @@ -0,0 +1,35 @@ +package matchers + +import ( + "fmt" + "path" + "strings" +) + +// Path is a pathname matcher. +// +// A value matches if it is the same as the matcher pattern or has the matcher +// pattern as a trailing component. For example, +// a pattern "abc/def" matches "abc/def" itself, "omg/abc/def", +// but not "abc/def/wtf", "abc/omg/def", or "xabc/def". +// +// Both the pattern and the value are sanitized using path.Clean() before use. +// +// The empty pattern "" matches only the empty value "". +type Path string + +// Matches returns whether x is the matching pathname itself or ends with the +// matching pathname, inside another directory. +func (p Path) Matches(x interface{}) bool { + if s, ok := x.(string); ok { + p1 := path.Clean(string(p)) + s = path.Clean(s) + return s == p1 || strings.HasSuffix(s, "/"+p1) + } + return false +} + +// String returns the string representation of this pathname matcher. +func (p Path) String() string { + return fmt.Sprintf("", path.Clean(string(p))) +} diff --git a/gomock_matchers/path_test.go b/gomock_matchers/path_test.go new file mode 100644 index 000000000..20c83d251 --- /dev/null +++ b/gomock_matchers/path_test.go @@ -0,0 +1,51 @@ +package matchers + +import "testing" + +func TestPathMatcher_Matches(t *testing.T) { + tests := []struct { + name string + p Path + x interface{} + want bool + }{ + {"EmptyMatchesEmpty", "", "", true}, + {"EmptyDoesNotMatchNonEmpty", "", "a", false}, + {"EmptyDoesNotMatchNonEmptyEvenWithTrailingSlash", "", "a/", false}, + {"NonEmptyDoesNotMatchEmpty", "a", "", false}, + {"ExactIsOK", "abc/def", "abc/def", true}, + {"SuffixIsOK", "abc/def", "omg/abc/def", true}, + {"SubstringIsNotOK", "abc/def", "omg/abc/def/wtf", false}, + {"PrefixIsNotOK", "abc/def", "abc/def/wtf", false}, + {"InterveningElementIsNotOK", "abc/def", "abc/bbq/def", false}, + {"GeneralNonMatch", "abc/def", "omg/wtf", false}, + {"UncleanPattern", "abc//def/", "abc/def", true}, + {"UncleanArg", "abc/def", "abc//def", true}, + {"NonStringArg", "a", 'a', false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.Matches(tt.x); got != tt.want { + t.Errorf("Path.Matches() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPathMatcher_String(t *testing.T) { + tests := []struct { + name string + p Path + want string + }{ + {"General", "abc/def", ""}, + {"Unclean", "abc//def/", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.String(); got != tt.want { + t.Errorf("Path.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/gomock_matchers/slice.go b/gomock_matchers/slice.go new file mode 100644 index 000000000..31dfc8997 --- /dev/null +++ b/gomock_matchers/slice.go @@ -0,0 +1,39 @@ +package matchers + +import ( + "fmt" + "reflect" +) + +// Slice is a gomock matcher that matches elements of an array or +// slice against its own members at the corresponding positions. +// Each member item in a Slice may be a regular item or a gomock +// Matcher instance. +type Slice []interface{} + +// Matches returns whether x is a slice with matching elements. +func (sm Slice) Matches(x interface{}) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Slice, reflect.Array: // OK + default: + return false + } + l := v.Len() + if l != len(sm) { + return false + } + for i, m := range sm { + m1 := toMatcher(m) + v1 := v.Index(i).Interface() + if !m1.Matches(v1) { + return false + } + } + return true +} + +// String returns the string representation of this slice matcher. +func (sm Slice) String() string { + return fmt.Sprint([]interface{}(sm)) +} diff --git a/gomock_matchers/slice_test.go b/gomock_matchers/slice_test.go new file mode 100644 index 000000000..bf9c3bad3 --- /dev/null +++ b/gomock_matchers/slice_test.go @@ -0,0 +1,114 @@ +package matchers + +import ( + "testing" + + "github.com/golang/mock/gomock" +) + +func TestSliceMatcher_Matches(t *testing.T) { + tests := []struct { + name string + sm Slice + x interface{} + want bool + }{ + { + "EmptyEqEmpty", + Slice{}, + []interface{}{}, + true, + }, + { + "EmptyNeNotEmpty", + Slice{}, + []interface{}{1}, + false, + }, + { + "NotEmptyNeEmpty", + Slice{0}, + []interface{}{}, + false, + }, + { + "CompareRawValuesUsingEqualityHappy", + Slice{1, 2, 3}, + []interface{}{1, 2, 3}, + true, + }, + { + "CompareRawValuesUsingEqualityUnhappy", + Slice{1, 2, 3}, + []interface{}{1, 20, 3}, + false, + }, + { + "CompareMatcherUsingItsMatchesHappy", + Slice{gomock.Nil(), gomock.Eq(3)}, + []interface{}{nil, 3}, + true, + }, + { + "CompareMatcherUsingItsMatchesUnhappy", + Slice{gomock.Nil(), gomock.Eq(3)}, + []interface{}{0, 3}, + false, + }, + { + "NestedHappy", + Slice{Slice{3}, 30}, + []interface{}{[]interface{}{3}, 30}, + true, + }, + { + "NestedUnhappy", + Slice{Slice{3}, 30}, + []interface{}{[]interface{}{300}, 30}, + false, + }, + { + "MatchSliceOfMoreSpecificTypes", + Slice{1, 2, 3}, + []int{1, 2, 3}, + true, + }, + { + "AcceptArraysToo", + Slice{1, 2, 3}, + [...]int{1, 2, 3}, + true, + }, + { + "RejectString", + Slice{'a', 'b', 'c'}, + "abc", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.sm.Matches(tt.x); got != tt.want { + t.Errorf("Slice.Matches() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSliceMatcher_String(t *testing.T) { + tests := []struct { + name string + sm Slice + want string + }{ + {"int", []interface{}{3, 5, 7}, "[3 5 7]"}, + {"string", []interface{}{"omg", "wtf", "bbq"}, "[omg wtf bbq]"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.sm.String(); got != tt.want { + t.Errorf("Slice.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/gomock_matchers/struct.go b/gomock_matchers/struct.go new file mode 100644 index 000000000..d1b344eed --- /dev/null +++ b/gomock_matchers/struct.go @@ -0,0 +1,53 @@ +package matchers + +import ( + "fmt" + "reflect" + "sort" + "strings" +) + +// Struct is a struct member matcher. +type Struct map[string]interface{} + +// Matches returns whether all specified members match. +func (sm Struct) Matches(x interface{}) bool { + v := reflect.ValueOf(x) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return false + } + for n, m := range sm { + m1 := toMatcher(m) + f := v.FieldByName(n) + if f == (reflect.Value{}) { + return false + } + f1 := f.Interface() + if !m1.Matches(f1) { + return false + } + } + return true +} + +func (sm Struct) String() string { + var fields sort.StringSlice + for name := range sm { + fields = append(fields, name) + } + sort.Sort(fields) + for i, name := range fields { + value := sm[name] + var vs string + if _, ok := value.(fmt.Stringer); ok { + vs = fmt.Sprintf("%s", value) + } else { + vs = fmt.Sprintf("%v", value) + } + fields[i] = fmt.Sprintf("%s=%s", name, vs) + } + return "" +} diff --git a/gomock_matchers/struct_test.go b/gomock_matchers/struct_test.go new file mode 100644 index 000000000..66ce62d70 --- /dev/null +++ b/gomock_matchers/struct_test.go @@ -0,0 +1,113 @@ +package matchers + +import ( + "testing" + + "github.com/golang/mock/gomock" +) + +type stringable int + +func (s stringable) String() string { + return "omg" +} + +func TestStructMatcher_Matches(t *testing.T) { + type value struct { + A int + B string + } + tests := []struct { + name string + sm Struct + x interface{} + want bool + }{ + { + "EmptyMatchesEmpty", + Struct{}, + value{}, + true, + }, + { + "EmptyMatchesAny", + Struct{}, + value{A: 3, B: "omg"}, + true, + }, + { + "EmptyStillDoesNotMatchNonStruct", + Struct{}, + 0, + false, + }, + { + "RegularFieldValuesUseEq1", + Struct{"A": 3, "B": "omg"}, + value{A: 3, B: "omg"}, + true, + }, + { + "RegularFieldValuesUseEq2", + Struct{"A": 3, "B": "omg"}, + value{A: 4, B: "omg"}, + false, + }, + { + "MatchersAreUsedVerbatim1", + Struct{"A": gomock.Not(3), "B": gomock.Eq("omg")}, + value{A: 4, B: "omg"}, + true, + }, + { + "MatchersAreUsedVerbatim2", + Struct{"A": gomock.Not(3), "B": gomock.Eq("omg")}, + value{A: 3, B: "omg"}, + false, + }, + { + "UnspecifiedFieldsAreIgnored", + Struct{"A": 3}, + value{A: 3, B: "omg"}, + true, + }, + { + "MissingFieldsReturnFailure", + Struct{"NOTFOUND": 3}, + value{A: 3}, + false, + }, + { + "DerefsPointer", + Struct{"A": 3}, + &value{A: 3}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.sm.Matches(tt.x); got != tt.want { + t.Errorf("Struct.Matches() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructMatcher_String(t *testing.T) { + tests := []struct { + name string + sm Struct + want string + }{ + {"UsesStringer", Struct{"A": stringable(0)}, ""}, + {"ReprIfNotStringable", Struct{"A": nil}, ">"}, + {"SortsByKey", Struct{"B": 3, "A": 4}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.sm.String(); got != tt.want { + t.Errorf("Struct.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/gomock_matchers/util.go b/gomock_matchers/util.go new file mode 100644 index 000000000..9de9b2530 --- /dev/null +++ b/gomock_matchers/util.go @@ -0,0 +1,12 @@ +package matchers + +import ( + "github.com/golang/mock/gomock" +) + +func toMatcher(v interface{}) gomock.Matcher { + if m, ok := v.(gomock.Matcher); ok { + return m + } + return gomock.Eq(v) +}