diff --git a/gomock/matchers.go b/gomock/matchers.go index 079eae9..c172550 100644 --- a/gomock/matchers.go +++ b/gomock/matchers.go @@ -17,6 +17,7 @@ package gomock import ( "fmt" "reflect" + "regexp" "strings" ) @@ -168,6 +169,25 @@ func (n notMatcher) String() string { return "not(" + n.m.String() + ")" } +type regexMatcher struct { + regex *regexp.Regexp +} + +func (m regexMatcher) Matches(x any) bool { + switch t := x.(type) { + case string: + return m.regex.MatchString(t) + case []byte: + return m.regex.Match(t) + default: + return false + } +} + +func (m regexMatcher) String() string { + return "matches regex " + m.regex.String() +} + type assignableToTypeOfMatcher struct { targetType reflect.Type } @@ -382,6 +402,18 @@ func Not(x any) Matcher { return notMatcher{Eq(x)} } +// Regex checks whether parameter matches the associated regex. +// +// Example usage: +// +// Regex("[0-9]{2}:[0-9]{2}").Matches("23:02") // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches([]byte{'2', '3', ':', '0', '2'}) // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches("hello world") // returns false +// Regex("[0-9]{2}").Matches(21) // returns false as it's not a valid type +func Regex(regexStr string) Matcher { + return regexMatcher{regex: regexp.MustCompile(regexStr)} +} + // AssignableToTypeOf is a Matcher that matches if the parameter to the mock // function is assignable to the type of the parameter to this function. // diff --git a/gomock/matchers_test.go b/gomock/matchers_test.go index 0380aa8..efb03ee 100644 --- a/gomock/matchers_test.go +++ b/gomock/matchers_test.go @@ -47,6 +47,7 @@ func TestMatchers(t *testing.T) { []e{nil, (error)(nil), (chan bool)(nil), (*int)(nil)}, []e{"", 0, make(chan bool), errors.New("err"), new(int)}}, {"test Not", gomock.Not(gomock.Eq(4)), []e{3, "blah", nil, int64(4)}, []e{4}}, + {"test Regex", gomock.Regex("[0-9]{2}:[0-9]{2}"), []e{"23:02", "[23:02]: Hello world", []byte("23:02")}, []e{4, "23-02", "hello world", true, []byte("23-02")}}, {"test All", gomock.All(gomock.Any(), gomock.Eq(4)), []e{4}, []e{3, "blah", nil, int64(4)}}, {"test Len", gomock.Len(2), []e{[]int{1, 2}, "ab", map[string]int{"a": 0, "b": 1}, [2]string{"a", "b"}}, @@ -92,6 +93,65 @@ func TestNotMatcher(t *testing.T) { } } +// A more thorough test of regexMatcher +func TestRegexMatcher(t *testing.T) { + tests := []struct { + name string + regex string + input any + wantMatch bool + wantStringResponse string + shouldPanic bool + }{ + { + name: "match for whole num regex with start and end position matching", + regex: "^\\d+$", + input: "2302", + wantMatch: true, + wantStringResponse: "matches regex ^\\d+$", + }, + { + name: "match for valid regex with start and end position matching on longer string", + regex: "^[0-9]{2}:[0-9]{2}$", + input: "[23:02]: Hello world", + wantMatch: false, + wantStringResponse: "matches regex ^[0-9]{2}:[0-9]{2}$", + }, + { + name: "match for valid regex with struct as bytes", + regex: `^{"id":[0-9]{2}}$`, + input: []byte{'{', '"', 'i', 'd', '"', ':', '1', '2', '}'}, + wantMatch: true, + wantStringResponse: `matches regex ^{"id":[0-9]{2}}$`, + }, + { + name: "should panic when regex fails to compile", + regex: `^[0-9]\\?{2}:[0-9]{2}$`, + shouldPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + defer func() { _ = recover() }() + } + matcher := gomock.Regex(tt.regex) + + if tt.shouldPanic { + t.Errorf("test did not panic, and should have") + } + + if got := matcher.Matches(tt.input); got != tt.wantMatch { + t.Errorf("got = %v, wantMatch = %v", got, tt.wantMatch) + } + if gotStr := matcher.String(); gotStr != tt.wantStringResponse { + t.Errorf("got string = %v, want string = %v", gotStr, tt.wantStringResponse) + } + }) + } +} + type Dog struct { Breed, Name string }