diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9489faf --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/oxyno-zeta/gomock-extra-matcher + +go 1.15 + +require github.com/golang/mock v1.4.4 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11ec9a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/map-matcher.go b/map-matcher.go new file mode 100644 index 0000000..33fc488 --- /dev/null +++ b/map-matcher.go @@ -0,0 +1,143 @@ +package extra + +import ( + "fmt" + "reflect" + + "github.com/golang/mock/gomock" +) + +// MMatcher interface improved to add functions +type MMatcher interface { + gomock.Matcher + // Add a matcher for a specific key + Key(key interface{}, match interface{}) MMatcher +} + +type mStorage struct { + key interface{} + match interface{} +} + +type mapMatcher struct { + keys []*mStorage +} + +func (m *mapMatcher) String() string { + str := "" + + // Loop over all keys + for in, v := range m.keys { + // Add separator for display + if in > 0 { + str += ", " + } + + // Check if key is a gomock matcher + m, ok := v.key.(gomock.Matcher) + if ok { + str += fmt.Sprintf("key %s", m.String()) + } else { + str += fmt.Sprintf("key %v", v.key) + } + + // Try to cast to a matcher interface + m, ok = v.match.(gomock.Matcher) + // Check if cast is ok + if ok { + str += fmt.Sprintf(" must match %s", m.String()) + } else { + str += fmt.Sprintf(" must be equal to %v", v.match) + } + } + + return str +} + +func (m *mapMatcher) Matches(x interface{}) bool { + // Check if x is nil + if x == nil { + return false + } + + // Value of interface input + rval := reflect.ValueOf(x) + rkind := rval.Kind() + // Check if reflect value is supported or not + if rkind != reflect.Map { + return false + } + + // Default case + res := len(m.keys) != 0 + + // Create reflect indirect + // indirect := reflect.Indirect(rval) + // Loop over all matcher keys + for _, kk := range m.keys { + // Store if matcher key can be found + matchKeyFound := false + // Loop over map keys + for _, kVal := range rval.MapKeys() { + // Get key data + keyD := kVal.Interface() + // Get reflect value from key + rv := rval.MapIndex(kVal) + // Get data from key + val := rv.Interface() + // Check if matcher key is matching current key + if !isMatchingData(kk.key, keyD) { + // Skip this key + continue + } + + // Set match key as found + matchKeyFound = true + + // Check map key value is matching + res = res && isMatchingData(kk.match, val) + + // Break the loop at this step + // No need to continue to check map values + break + } + + // Check if match key was found + if !matchKeyFound { + // Match key wasn't found, it is an error + return false + } + + // Check result + if !res { + // If result isn't true at this step, stop now + return false + } + } + + return res +} + +func isMatchingData(matchKey, keyData interface{}) bool { + // Check if given key in matcher is a gomock matcher + mk, ok := matchKey.(gomock.Matcher) + if ok { + return mk.Matches(keyData) + } + + return matchKey == keyData +} + +func (m *mapMatcher) Key(key interface{}, match interface{}) MMatcher { + // Check if key exists + if key == nil { + return m + } + // Key name exists => add data + m.keys = append(m.keys, &mStorage{key: key, match: match}) + // Return + return m +} + +// MapMatcher will return a new map matcher +func MapMatcher() MMatcher { return &mapMatcher{} } diff --git a/map-matcher_test.go b/map-matcher_test.go new file mode 100644 index 0000000..7a8d0db --- /dev/null +++ b/map-matcher_test.go @@ -0,0 +1,336 @@ +package extra + +import ( + "reflect" + "testing" + + "github.com/golang/mock/gomock" +) + +func Test_mapMatcher_Matches(t *testing.T) { + starStrFunc := func(s string) *string { return &s } + type fakeStruct struct { + Bo2 bool + } + type fields struct { + keys []*mStorage + } + type args struct { + x interface{} + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "nil input", + args: args{x: nil}, + want: false, + }, + { + name: "bool input", + args: args{x: false}, + want: false, + }, + { + name: "string input", + args: args{x: "string"}, + want: false, + }, + { + name: "string pointer input", + args: args{x: starStrFunc("string")}, + want: false, + }, + { + name: "int input", + args: args{x: 8}, + want: false, + }, + { + name: "struct pointer input", + args: args{x: &fakeStruct{}}, + want: false, + }, + { + name: "struct input", + args: args{x: fakeStruct{}}, + want: false, + }, + { + name: "func input", + args: args{x: func() {}}, + want: false, + }, + { + name: "key not found on map[*string][string]", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: "data"}, + }, + }, + args: args{ + x: map[*string]string{ + starStrFunc("fake"): "data", + }, + }, + want: false, + }, + { + name: "key not found on map[string][string]", + fields: fields{ + keys: []*mStorage{ + {key: "fake2", match: "data"}, + }, + }, + args: args{ + x: map[string]string{ + "fake": "data", + }, + }, + want: false, + }, + { + name: "shouldn't match with no keys in matcher", + args: args{ + x: map[*string]string{ + starStrFunc("fake"): "data", + }, + }, + want: false, + }, + { + name: "shouldn't match map[*string][string] with value", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Eq(starStrFunc("fake")), match: "data-fake"}, + }, + }, + args: args{ + x: map[*string]string{ + starStrFunc("fake"): "data", + }, + }, + want: false, + }, + { + name: "shouldn't match map[string][string] with value", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: "data-fake"}, + }, + }, + args: args{ + x: map[string]string{ + "fake": "data", + }, + }, + want: false, + }, + { + name: "should match map[*string][string] with value", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Eq(starStrFunc("fake")), match: "data"}, + }, + }, + args: args{ + x: map[*string]string{ + starStrFunc("fake"): "data", + }, + }, + want: true, + }, + { + name: "should match map[*string][string] with value and a key ingored", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Eq(starStrFunc("fake")), match: "data"}, + }, + }, + args: args{ + x: map[*string]string{ + starStrFunc("fake"): "data", + starStrFunc("fake2"): "data2", + }, + }, + want: true, + }, + { + name: "should match map[*string][*string] with a gomock matcher", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Eq(starStrFunc("fake")), match: gomock.Eq(starStrFunc("data"))}, + }, + }, + args: args{ + x: map[*string]*string{ + starStrFunc("fake"): starStrFunc("data"), + }, + }, + want: true, + }, + { + name: "should match map[string][*struct] with a gomock matcher", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: gomock.Eq(&fakeStruct{ + Bo2: true, + })}, + }, + }, + args: args{ + x: map[string]*fakeStruct{ + "fake": {Bo2: true}, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mapMatcher{ + keys: tt.fields.keys, + } + if got := m.Matches(tt.args.x); got != tt.want { + t.Errorf("mapMatcher.Matches() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_mapMatcher_String(t *testing.T) { + type fields struct { + keys []*mStorage + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "empty", + want: "", + }, + { + name: "with gomock matcher as key and value", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Any(), match: gomock.Nil()}, + }, + }, + want: "key is anything must match is nil", + }, + { + name: "with gomock matcher as key and value as value", + fields: fields{ + keys: []*mStorage{ + {key: gomock.Any(), match: "data"}, + }, + }, + want: "key is anything must be equal to data", + }, + { + name: "with value as key and value as value", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: "data"}, + }, + }, + want: "key fake must be equal to data", + }, + { + name: "with value as key and value as value", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: "data"}, + }, + }, + want: "key fake must be equal to data", + }, + { + name: "with value as key and gomock matcher as value", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: gomock.Any()}, + }, + }, + want: "key fake must match is anything", + }, + { + name: "with multiple keys", + fields: fields{ + keys: []*mStorage{ + {key: "fake", match: gomock.Any()}, + {key: "fake2", match: gomock.Any()}, + }, + }, + want: "key fake must match is anything, key fake2 must match is anything", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mapMatcher{ + keys: tt.fields.keys, + } + if got := m.String(); got != tt.want { + t.Errorf("mapMatcher.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_mapMatcher_Key(t *testing.T) { + type fields struct { + keys []*mStorage + } + type args struct { + key interface{} + match interface{} + } + tests := []struct { + name string + fields fields + args args + want []*mStorage + }{ + { + name: "should ingore when key is nil", + want: nil, + }, + { + name: "empty matcher", + args: args{key: "fake"}, + want: []*mStorage{ + {key: "fake"}, + }, + }, + { + name: "add already existing key", + fields: fields{ + keys: []*mStorage{{ + key: "fake", + match: "value", + }}, + }, + args: args{key: "fake", match: gomock.Eq("value1")}, + want: []*mStorage{ + {key: "fake", match: "value"}, + {key: "fake", match: gomock.Eq("value1")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mapMatcher{ + keys: tt.fields.keys, + } + m.Key(tt.args.key, tt.args.match) + if !reflect.DeepEqual(m.keys, tt.want) { + t.Errorf("mapMatcher.Key() = %v, want %v", m.keys, tt.want) + } + }) + } +} diff --git a/struct-matcher.go b/struct-matcher.go new file mode 100644 index 0000000..6d7f8f6 --- /dev/null +++ b/struct-matcher.go @@ -0,0 +1,117 @@ +package extra + +import ( + "fmt" + "reflect" + + "github.com/golang/mock/gomock" +) + +// StMatcher interface improved to add functions +type StMatcher interface { + gomock.Matcher + // Add a matcher for a specific field + Field(fName string, match interface{}) StMatcher +} + +type sStorage struct { + fName string + match interface{} +} + +type structMatcher struct { + fields []*sStorage +} + +func (f *structMatcher) String() string { + str := "" + + // Loop over all fields + for in, v := range f.fields { + // Add separator for display + if in > 0 { + str += ", " + } + + str += fmt.Sprintf("field %s", v.fName) + + // Try to cast to a matcher interface + m, ok := v.match.(gomock.Matcher) + // Check if cast is ok + if ok { + str += fmt.Sprintf(" must match %s", m.String()) + } else { + str += fmt.Sprintf(" must be equal to %v", v.match) + } + } + + return str +} + +func (f *structMatcher) Field(fName string, match interface{}) StMatcher { + // Check if field name exists + if fName == "" { + return f + } + // Field name exists => add data + f.fields = append(f.fields, &sStorage{fName: fName, match: match}) + // Return + return f +} + +func (f *structMatcher) Matches(x interface{}) bool { + // Check if x is nil + if x == nil { + return false + } + + // Value of interface input + rval := reflect.ValueOf(x) + rkind := rval.Kind() + // Check if reflect value is supported or not + if rkind != reflect.Struct && rkind != reflect.Ptr { + return false + } + + // Default case + res := len(f.fields) != 0 + + // Create reflect indirect + indirect := reflect.Indirect(rval) + // Loop over all fields + for _, v := range f.fields { + // Try to get field value + fval := indirect.FieldByName(v.fName) + // Check if field doesn't exist + if !fval.IsValid() { + // In this case returning false is ok + // This case appears when the field isn't found in structure + return false + } + + // Get data from field + data := fval.Interface() + + // Try to cast a gomock matcher + m, ok := v.match.(gomock.Matcher) + // Check if cast is ok + if ok { + // Run matcher + res = res && m.Matches(data) + } else { + res = res && v.match == data + } + + // Check result + if !res { + // If result isn't true at this step, stop now + return false + } + } + + // Default case + return res +} + +// StructMatcher will return a new struct matcher +func StructMatcher() StMatcher { return &structMatcher{} } diff --git a/struct-matcher_test.go b/struct-matcher_test.go new file mode 100644 index 0000000..e56d7f0 --- /dev/null +++ b/struct-matcher_test.go @@ -0,0 +1,325 @@ +package extra + +import ( + "reflect" + "testing" + + "github.com/golang/mock/gomock" +) + +func Test_structMatcher_Field(t *testing.T) { + type fields struct { + fields []*sStorage + } + type args struct { + fName string + matcher interface{} + } + tests := []struct { + name string + fields fields + args args + want []*sStorage + }{ + { + name: "empty field name", + }, + { + name: "empty matcher", + args: args{fName: "fake"}, + want: []*sStorage{{ + fName: "fake", + }}, + }, + { + name: "add already existing field", + fields: fields{ + fields: []*sStorage{{ + fName: "fake", + match: "value", + }}, + }, + args: args{fName: "fake", matcher: gomock.Eq("value1")}, + want: []*sStorage{ + {fName: "fake", match: "value"}, + {fName: "fake", match: gomock.Eq("value1")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &structMatcher{ + fields: tt.fields.fields, + } + f.Field(tt.args.fName, tt.args.matcher) + if !reflect.DeepEqual(f.fields, tt.want) { + t.Errorf("structMatcher.Field() = %v, want %v", f.fields, tt.want) + } + }) + } +} + +func Test_structMatcher_String(t *testing.T) { + type fields struct { + fields []*sStorage + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "empty", + want: "", + }, + { + name: "gomock matcher", + fields: fields{ + fields: []*sStorage{{ + fName: "fake", + match: gomock.Nil(), + }}, + }, + want: "field fake must match is nil", + }, + { + name: "match value", + fields: fields{ + fields: []*sStorage{{ + fName: "fake", + match: "value", + }}, + }, + want: "field fake must be equal to value", + }, + { + name: "gomock matcher and match value", + fields: fields{ + fields: []*sStorage{{ + fName: "fake", + match: gomock.Nil(), + }, { + fName: "fake2", + match: "value", + }}, + }, + want: "field fake must match is nil, field fake2 must be equal to value", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &structMatcher{ + fields: tt.fields.fields, + } + if got := f.String(); got != tt.want { + t.Errorf("structMatcher.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_structMatcher_Matches(t *testing.T) { + starStrFunc := func(s string) *string { return &s } + type innerFakeStruct struct { + Bo2 bool + Stp2 *string + St2 string + } + type fakeStruct struct { + Bo bool + St string + Stp *string + I int + Mm map[string]string + InnerP *innerFakeStruct + Inner innerFakeStruct + } + + type fields struct { + fields []*sStorage + } + type args struct { + x interface{} + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "nil input", + args: args{x: nil}, + want: false, + }, + { + name: "bool input", + args: args{x: false}, + want: false, + }, + { + name: "string input", + args: args{x: "string"}, + want: false, + }, + { + name: "string pointer input", + args: args{x: starStrFunc("string")}, + want: false, + }, + { + name: "int input", + args: args{x: 8}, + want: false, + }, + { + name: "map input", + args: args{x: map[string]string{}}, + want: false, + }, + { + name: "func input", + args: args{x: func() {}}, + want: false, + }, + { + name: "shouldn't match first level struct", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: starStrFunc("fake")}, + {fName: "St", match: "fake"}, + }, + }, + args: args{ + x: fakeStruct{}, + }, + want: false, + }, + { + name: "shouldn't match first level struct 2", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: starStrFunc("fake")}, + {fName: "St", match: "fake"}, + }, + }, + args: args{ + x: fakeStruct{Stp: starStrFunc("fake2")}, + }, + want: false, + }, + { + name: "should match first level struct", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: gomock.Eq(starStrFunc("fake"))}, + {fName: "St", match: "fake"}, + }, + }, + args: args{ + x: fakeStruct{ + Stp: starStrFunc("fake"), + St: "fake", + }, + }, + want: true, + }, + { + name: "shouldn't match first level struct because field not found", + fields: fields{ + fields: []*sStorage{ + {fName: "Stfake", match: "fake"}, + }, + }, + args: args{ + x: fakeStruct{ + Stp: starStrFunc("fake"), + St: "fake", + }, + }, + want: false, + }, + { + name: "should match first level struct (pointer)", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: gomock.Eq(starStrFunc("fake"))}, + {fName: "St", match: "fake"}, + }, + }, + args: args{ + x: &fakeStruct{ + Stp: starStrFunc("fake"), + St: "fake", + }, + }, + want: true, + }, + { + name: "should match first level struct (map)", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: gomock.Eq(starStrFunc("fake"))}, + {fName: "St", match: "fake"}, + {fName: "Mm", match: gomock.Eq(map[string]string{"fake1": "fake1"})}, + }, + }, + args: args{ + x: &fakeStruct{ + Stp: starStrFunc("fake"), + St: "fake", + Mm: map[string]string{"fake1": "fake1"}, + }, + }, + want: true, + }, + { + name: "should match first and second level struct", + fields: fields{ + fields: []*sStorage{ + {fName: "Stp", match: gomock.Any()}, + {fName: "Stp", match: gomock.Eq(starStrFunc("fake"))}, + {fName: "St", match: "fake"}, + { + fName: "Inner", + match: StructMatcher().Field("St2", "fake2").Field("Stp2", gomock.Eq(starStrFunc("fake2"))), + }, + { + fName: "InnerP", + match: StructMatcher().Field("St2", "fake2").Field("Stp2", gomock.Eq(starStrFunc("fake2"))), + }, + }, + }, + args: args{ + x: &fakeStruct{ + Stp: starStrFunc("fake"), + St: "fake", + Inner: innerFakeStruct{ + Stp2: starStrFunc("fake2"), + St2: "fake2", + }, + InnerP: &innerFakeStruct{ + Stp2: starStrFunc("fake2"), + St2: "fake2", + }, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &structMatcher{ + fields: tt.fields.fields, + } + if got := f.Matches(tt.args.x); got != tt.want { + t.Errorf("structMatcher.Matches() = %v, want %v", got, tt.want) + } + }) + } +}