-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Support iter.Seq
s in [Not]Contains
and [Not]ElementsMatch
#1685
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
//go:build go1.23 || goexperiment.rangefunc | ||
|
||
package assert | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
// go.mod version is set to 1.17, which precludes the use of generics (even though this file wouldn't be taken into | ||
// account per the build tags). | ||
|
||
func intSeq(s ...int) func(yield func(int) bool) { | ||
return func(yield func(int) bool) { | ||
for _, elem := range s { | ||
if !yield(elem) { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
func strSeq(s ...string) func(yield func(string) bool) { | ||
return func(yield func(string) bool) { | ||
for _, elem := range s { | ||
if !yield(elem) { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
func TestElementsMatch_Seq(t *testing.T) { | ||
mockT := new(testing.T) | ||
|
||
cases := []struct { | ||
expected interface{} | ||
actual interface{} | ||
result bool | ||
}{ | ||
{intSeq(), intSeq(), true}, | ||
{intSeq(1), intSeq(1), true}, | ||
{intSeq(1, 1), intSeq(1, 1), true}, | ||
{intSeq(1, 2), intSeq(1, 2), true}, | ||
{intSeq(1, 2), intSeq(2, 1), true}, | ||
{strSeq("hello", "world"), strSeq("world", "hello"), true}, | ||
{strSeq("hello", "hello"), strSeq("hello", "hello"), true}, | ||
{strSeq("hello", "hello", "world"), strSeq("hello", "world", "hello"), true}, | ||
{intSeq(), nil, true}, | ||
|
||
// not matching | ||
{intSeq(1), intSeq(1, 1), false}, | ||
{intSeq(1, 2), intSeq(2, 2), false}, | ||
{strSeq("hello", "hello"), strSeq("hello"), false}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(fmt.Sprintf("ElementsMatch(%#v, %#v)", seqToSlice(c.expected), seqToSlice(c.actual)), func(t *testing.T) { | ||
res := ElementsMatch(mockT, c.actual, c.expected) | ||
|
||
if res != c.result { | ||
t.Errorf("ElementsMatch(%#v, %#v) should return %v", seqToSlice(c.actual), seqToSlice(c.expected), c.result) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestNotElementsMatch_Seq(t *testing.T) { | ||
mockT := new(testing.T) | ||
|
||
cases := []struct { | ||
expected interface{} | ||
actual interface{} | ||
result bool | ||
}{ | ||
// not matching | ||
{intSeq(1), intSeq(), true}, | ||
{intSeq(), intSeq(2), true}, | ||
{intSeq(1), intSeq(2), true}, | ||
{intSeq(1), intSeq(1, 1), true}, | ||
{intSeq(1, 2), intSeq(3, 4), true}, | ||
{intSeq(3, 4), intSeq(1, 2), true}, | ||
{intSeq(1, 1, 2, 3), intSeq(1, 2, 3), true}, | ||
{strSeq("hello"), strSeq("world"), true}, | ||
{strSeq("hello", "hello"), strSeq("world", "world"), true}, | ||
|
||
// matching | ||
{intSeq(), nil, false}, | ||
{intSeq(), intSeq(), false}, | ||
{intSeq(1), intSeq(1), false}, | ||
{intSeq(1, 1), intSeq(1, 1), false}, | ||
{intSeq(1, 2), intSeq(2, 1), false}, | ||
{intSeq(1, 1, 2), intSeq(1, 2, 1), false}, | ||
{strSeq("hello", "world"), strSeq("world", "hello"), false}, | ||
{strSeq("hello", "hello"), strSeq("hello", "hello"), false}, | ||
{strSeq("hello", "hello", "world"), strSeq("hello", "world", "hello"), false}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(fmt.Sprintf("NotElementsMatch(%#v, %#v)", seqToSlice(c.expected), seqToSlice(c.actual)), func(t *testing.T) { | ||
res := NotElementsMatch(mockT, c.actual, c.expected) | ||
|
||
if res != c.result { | ||
t.Errorf("NotElementsMatch(%#v, %#v) should return %v", seqToSlice(c.actual), seqToSlice(c.expected), c.result) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestContainsNotContains_Seq(t *testing.T) { | ||
|
||
type A struct { | ||
Name, Value string | ||
} | ||
complexSeq := func(s ...*A) func(yield func(*A) bool) { | ||
return func(yield func(*A) bool) { | ||
for _, elem := range s { | ||
if !yield(elem) { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
list := []string{"Foo", "Bar"} | ||
|
||
complexList := []*A{ | ||
{"b", "c"}, | ||
{"d", "e"}, | ||
{"g", "h"}, | ||
{"j", "k"}, | ||
} | ||
|
||
cases := []struct { | ||
expected interface{} | ||
actual interface{} | ||
result bool | ||
}{ | ||
{strSeq(list...), "Bar", true}, | ||
{strSeq(list...), "Salut", false}, | ||
{complexSeq(complexList...), &A{"g", "h"}, true}, | ||
{complexSeq(complexList...), &A{"g", "e"}, false}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(fmt.Sprintf("Contains(%#v, %#v)", seqToSlice(c.expected), seqToSlice(c.actual)), func(t *testing.T) { | ||
mockT := new(testing.T) | ||
res := Contains(mockT, c.expected, c.actual) | ||
|
||
if res != c.result { | ||
if res { | ||
t.Errorf( | ||
"Contains(%#v, %#v) should return true:\n\t%#v contains %#v", | ||
seqToSlice(c.expected), seqToSlice(c.actual), seqToSlice(c.expected), seqToSlice(c.actual)) | ||
} else { | ||
t.Errorf( | ||
"Contains(%#v, %#v) should return false:\n\t%#v does not contain %#v", | ||
seqToSlice(c.expected), seqToSlice(c.actual), seqToSlice(c.expected), seqToSlice(c.actual)) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(fmt.Sprintf("NotContains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { | ||
mockT := new(testing.T) | ||
res := NotContains(mockT, c.expected, c.actual) | ||
|
||
// NotContains should be inverse of Contains. If it's not, something is wrong | ||
if res == Contains(mockT, c.expected, c.actual) { | ||
if res { | ||
t.Errorf("NotContains(%#v, %#v) should return true:\n\t%#v does not contains %#v", c.expected, c.actual, c.expected, c.actual) | ||
} else { | ||
t.Errorf("NotContains(%#v, %#v) should return false:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,44 @@ | ||||||||||
//go:build go1.23 || goexperiment.rangefunc | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This compile guard doesn't seem necessary. There is nothing in seqToSlice implementation that requires Go 1.23. Sequences functions can be implemented in Go below 1.23. Go 1.23 only adds syntactic sugar in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair. My thinking was that w/o range over func, there is no such thing as a "sequence" in Go and thus no basis for semantically treating a function of that form as a sequence (I am not aware of any use of the |
||||||||||
|
||||||||||
package assert | ||||||||||
|
||||||||||
import "reflect" | ||||||||||
|
||||||||||
var ( | ||||||||||
boolType = reflect.TypeOf(true) | ||||||||||
) | ||||||||||
|
||||||||||
// seqToSlice checks if x is a sequence, and converts it to a slice of the | ||||||||||
// same element type. Otherwise, x is returned as-is. | ||||||||||
func seqToSlice(x interface{}) interface{} { | ||||||||||
misberner marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
if x == nil { | ||||||||||
return nil | ||||||||||
} | ||||||||||
|
||||||||||
xv := reflect.ValueOf(x) | ||||||||||
xt := xv.Type() | ||||||||||
// We're looking for a function with exactly one input parameter and no return values. | ||||||||||
if xt.Kind() != reflect.Func || xt.NumIn() != 1 || xt.NumOut() != 0 { | ||||||||||
return x | ||||||||||
} | ||||||||||
|
||||||||||
// The input parameter should be of type func(T) bool | ||||||||||
paramType := xt.In(0) | ||||||||||
if paramType.Kind() != reflect.Func || paramType.NumIn() != 1 || paramType.NumOut() != 1 || paramType.Out(0) != boolType { | ||||||||||
return x | ||||||||||
} | ||||||||||
|
||||||||||
elemType := paramType.In(0) | ||||||||||
resultType := reflect.SliceOf(elemType) | ||||||||||
result := reflect.MakeSlice(resultType, 0, 0) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not necessary to allocate a zero element slice, as it will be discarded on the first call to append.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed not necessary, thanks for pointing this out. Though I'll go with this to save a
Suggested change
|
||||||||||
|
||||||||||
yieldFunc := reflect.MakeFunc(paramType, func(args []reflect.Value) []reflect.Value { | ||||||||||
result = reflect.Append(result, args[0]) | ||||||||||
return []reflect.Value{reflect.ValueOf(true)} | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allocate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, will do. |
||||||||||
}) | ||||||||||
|
||||||||||
// Call the function with the yield function as the argument | ||||||||||
xv.Call([]reflect.Value{yieldFunc}) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This extracts only the first element of the sequence. From the description of the function I expect the whole sequence to be serialized into the slice using a loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is incorrect, as otherwise obviously tests wouldn't be passing. The iteration itself happens in the function wrapped in |
||||||||||
|
||||||||||
return result.Interface() | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
//go:build !go1.23 && !goexperiment.rangefunc | ||
|
||
package assert | ||
|
||
// seqToSlice would convert a sequence of elements to a slice of the respective type. | ||
// However, since sequences are not supported given the build tags, it just returns x as-is. | ||
func seqToSlice(x interface{}) interface{} { | ||
return x | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a reason for this guard (see my other comment in seq_supported.go).