Skip to content

Commit

Permalink
Generics (#8)
Browse files Browse the repository at this point in the history
v.1.1.0 - generics

Changes all the matchers to use generics instead of reflection. Some still use a bit of reflection, e.g. TypeName etc.

Other major changes:

ValueContaining has been split into StringContaining, MapContaining, MapContainingValues, MapMatchingValues, ArrayContaining and ArrayMatching.
No longer panics with unknown types, as types will fail at compile time.
Some idiosyncrasies with the generic types do exist, but this is language specific;

map matchers generally need to know the type of the map key values explicitly or the compiler will complain, e.g.
then.AssertThat(testing, map[string]bool{"hi": true, "bye": true}, has.AllKeys[string, bool]("hi", "bye"))
has.Length() is likewise pernickety about types being explicit, mainly because it works on both strings and arrays. It needs to know both the type of the array and the array/string type. Confused? me too.
is.LessThan and is.GreaterThan no longer work on complex types. This is because the complex types do not support the comparison operators (yet, somehow, they could be compared by reflection 🤷 )

See the matcher_test.go file for full usage.

---------

Co-authored-by: mattcorby-eaglen <[email protected]>
  • Loading branch information
corbym and mattcorby-eaglen authored Apr 1, 2023
1 parent 38dfeaf commit c340bb4
Show file tree
Hide file tree
Showing 33 changed files with 870 additions and 612 deletions.
15 changes: 0 additions & 15 deletions .travis.yml

This file was deleted.

28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Expected: value equal to <bye>
Composed with AllOf:

```go
then.AssertThat(t, "abcdef", is.AllOf(is.ValueContaining("abc"), is.LessThan("ghi")))
then.AssertThat(t, "abcdef", is.AllOf(is.StringContaining("abc"), is.LessThan("ghi")))
```

Asynchronous Matching (v1.0.8 onwards):
Expand All @@ -60,12 +60,36 @@ then.WithinTenSeconds(t, func(eventually gocrest.TestingT) {
then.AssertThat(eventually, by.Channelling(channelTwo), is.EqualTo("11").Reason("This is unreachable"))
})
```
# v.1.1.0 - generics

Changes all the matchers to use generics instead of reflection. Some still use a bit of reflection, e.g. TypeName etc.

## Other major changes:

* ValueContaining has been split into StringContaining, MapContaining, MapContainingValues, MapMatchingValues, ArrayContaining and ArrayMatching.
* No longer panics with unknown types, as types will fail at compile time.
Some idiosyncrasies with the generic types do exist, but this is language specific;

* Map matchers generally need to know the type of the map key values explicitly or the compiler will complain, e.g.
```
then.AssertThat(testing, map[string]bool{"hi": true, "bye": true}, has.AllKeys[string, bool]("hi", "bye"))
```
* `has.Length()` is likewise pernickety about types being explicit, mainly because it works on both strings and arrays. It needs to know both the type of the array and the array/string type. Confused? me too.
* `is.LessThan()` and `is.GreaterThan()` (and by extension `is.GreaterThanOrEqualTo` and `is.LessThanOrEqualTo`) no longer work on complex types. This is because the complex types do not support the comparison operators (yet, somehow, they could be compared by reflection 🤷 )

See the matcher_test.go file for full usage.

# Matchers so far..

- is.EqualTo(x)
- is.EqualToIgnoringWhitespace(string) - compares two strings without comparing their whitespace characters.
- is.Nil() - value must be nil
- is.ValueContaining(expected) -- acts like containsAll
- is.StringContaining(expected) -- acts like containsAll
- is.MapContaining(expected) -- acts like containsAll
- is.MapContainingValues(expected) -- acts like containsAll
- is.MapMatchingValues(expected) -- acts like containsAll
- is.ArrayContaining(expected) -- acts like containsAll
- is.ArrayMatching(expected) -- acts like containsAll
- is.Not(m *Matcher) -- logical not of matcher's result
- is.MatchForPattern(regex string) -- a string regex expression
- has.FunctionNamed(string x) - checks if an interface has a function (method)
Expand Down
19 changes: 11 additions & 8 deletions by/eventually.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package by
import (
"bufio"
"io"
"reflect"
)

func Channelling(actual interface{}) interface{} {
var selectCase = make([]reflect.SelectCase, 1)
selectCase[0].Dir = reflect.SelectRecv
selectCase[0].Chan = reflect.ValueOf(actual)
_, recv, _ := reflect.Select(selectCase)
return recv.Interface()
// Channelling channels any channel of type T and returns the value from the channel
func Channelling[T any](actual chan T) T {
return <-actual
}
func Reading(actual io.Reader, len int) interface{} {

// Reading peeks at the value of a reader by reading `len` bytes ahead.
func Reading(actual io.Reader, len int) []byte {
reader := bufio.NewReader(actual.(io.Reader))
peek, _ := reader.Peek(len)
return peek
}

// Calling calls the function passed and returns the value
func Calling[K any, T any](actual func(T) K, value T) K {
return actual(value)
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
module github.com/corbym/gocrest

go 1.15
go 1.18

require (
golang.org/x/sys v0.4.0 // indirect
golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc // indirect
golang.org/x/tools/cmd/cover v0.1.0-deprecated // indirect
)
38 changes: 12 additions & 26 deletions has/haseveryelement.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,37 @@ package has

import (
"fmt"
"reflect"

"github.com/corbym/gocrest"
)

// EveryElement Checks whether the nth element of the array/slice matches the nth expectation passed
// Panics if the actual is not an array/slice
// Panics if the count of the expectations does not match the array's/slice's length
func EveryElement(expects ...*gocrest.Matcher) *gocrest.Matcher {
match := new(gocrest.Matcher)
func EveryElement[A any](expects ...*gocrest.Matcher[A]) *gocrest.Matcher[[]A] {
match := new(gocrest.Matcher[[]A])
match.Describe = fmt.Sprintf("elements to match %s", describe(expects, "and"))

for _, e := range expects {
match.AppendActual(e.Actual)
}

match.Matches = func(actual interface{}) bool {

actualValue := reflect.ValueOf(actual)
switch actualValue.Kind() {
case reflect.Array, reflect.Slice:
match.Matches = func(actual []A) bool {
if len(actual) != len(expects) {
return false
}

if actualValue.Len() != len(expects) {
for i := 0; i < len(actual); i++ {
result := expects[i].Matches(actual[i])
if !result {
return false
}

for i := 0; i < actualValue.Len(); i++ {
result := expects[i].Matches(actualValue.Index(i).Interface())

if !result {
return false
}
}

return true

default:
panic("cannot determine type of variadic actual, " + actualValue.String())
}

return true
}

return match
}

func describe(matchers []*gocrest.Matcher, conjunction string) string {
func describe[A any](matchers []*gocrest.Matcher[A], conjunction string) string {
var description string
for x := 0; x < len(matchers); x++ {
description += fmt.Sprintf("[%v]:%v", x, matchers[x].Describe)
Expand Down
6 changes: 3 additions & 3 deletions has/hasfield.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (

// FieldNamed is a naive implementation for testing if a struct has a particular field name. Does not check type.
// Returns a matcher that will use reflect to check if the actual has the method given by expected
func FieldNamed(expected string) *gocrest.Matcher {
matcher := new(gocrest.Matcher)
func FieldNamed[A any](expected string) *gocrest.Matcher[A] {
matcher := new(gocrest.Matcher[A])
matcher.Describe = fmt.Sprintf("struct with function %s", expected)
matcher.Matches = func(actual interface{}) bool {
matcher.Matches = func(actual A) bool {
typeOfActual := reflect.TypeOf(actual)
matcher.Actual = fieldStringValue(typeOfActual)
expectedName := reflect.ValueOf(expected).String()
Expand Down
6 changes: 3 additions & 3 deletions has/hasfunction.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (

// FunctionNamed implementation for testing if a Type has a particular method name. Does not check parameters.
// Returns a matcher that will use reflect to check if the actual has the method given by expected.
func FunctionNamed(expected string) *gocrest.Matcher {
matcher := new(gocrest.Matcher)
func FunctionNamed[A any](expected string) *gocrest.Matcher[A] {
matcher := new(gocrest.Matcher[A])
matcher.Describe = fmt.Sprintf("interface with function %s", expected)
matcher.Matches = func(actual interface{}) bool {
matcher.Matches = func(actual A) bool {
typeOfActual := reflect.TypeOf(actual)
matcher.Actual = actualStringValue(typeOfActual)
expectedName := reflect.ValueOf(expected).String()
Expand Down
38 changes: 13 additions & 25 deletions has/haskey.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,38 @@ package has
import (
"fmt"
"github.com/corbym/gocrest"
"reflect"
)

// Key is a matcher that checks if actual has a key == expected.
// Panics when actual's Kind is not a map.
// Returns a matcher that matches when a map has key == expected
func Key(expected interface{}) *gocrest.Matcher {
matcher := new(gocrest.Matcher)
matcher.Describe = fmt.Sprintf("map has key '%s'", expected)
matcher.Matches = func(actual interface{}) bool {
func Key[K comparable, V any](expected K) *gocrest.Matcher[map[K]V] {
matcher := new(gocrest.Matcher[map[K]V])
matcher.Describe = fmt.Sprintf("map has key '%v'", expected)
matcher.Matches = func(actual map[K]V) bool {
return hasKey(actual, expected)
}
return matcher
}

// AllKeys is a matcher that checks if map actual has all keys == expecteds.
// Panics when actual's Kind is not a map.
// Returns a matcher that matches when a map has all keys == all expected.
func AllKeys(expected ...interface{}) *gocrest.Matcher {
matcher := new(gocrest.Matcher)
matcher.Describe = fmt.Sprintf("map has keys '%s'", expected)
matcher.Matches = func(actual interface{}) bool {
keyValuesToMatch := reflect.ValueOf(correctExpectedValue(expected...))
for x := 0; x < keyValuesToMatch.Len(); x++ {
if !hasKey(actual, keyValuesToMatch.Index(x).Interface()) {
func AllKeys[K comparable, V any](expected ...K) *gocrest.Matcher[map[K]V] {
matcher := new(gocrest.Matcher[map[K]V])
matcher.Describe = fmt.Sprintf("map has keys '%v'", expected)
matcher.Matches = func(actual map[K]V) bool {
for _, k := range expected {
if !hasKey(actual, k) {
return false
}
}
return true
}
return matcher
}
func correctExpectedValue(expected ...interface{}) interface{} {
kind := reflect.ValueOf(expected[0]).Kind()
if kind == reflect.Slice {
return expected[0]
}
return expected
}

func hasKey(actual interface{}, expected interface{}) bool {
mapKeys := reflect.ValueOf(actual).MapKeys()
for x := 0; x < len(mapKeys); x++ {
if mapKeys[x].Interface() == expected {
func hasKey[K comparable, V any](actual map[K]V, expected K) bool {
for k := range actual {
if k == expected {
return true
}
}
Expand Down
65 changes: 49 additions & 16 deletions has/haslength.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,62 @@ package has
import (
"fmt"
"github.com/corbym/gocrest"
"reflect"
)

// Length can be called with arrays, maps, *gocrest.Matcher and strings but not numeric types.
// has.Length(is.GreaterThan(x)) is a valid call.
// Length can be called with arrays and strings
// Returns a matcher that matches if the length matches the given criteria
func Length(expected interface{}) *gocrest.Matcher {
func Length[V any, A []V | string](expected int) *gocrest.Matcher[A] {
const description = "value with length %v"
matcher := new(gocrest.Matcher)
matcher := new(gocrest.Matcher[A])
matcher.Describe = fmt.Sprintf(description, expected)
matcher.Matches = func(actual interface{}) bool {
if actual == nil {
return false
}
lenOfActual := reflect.ValueOf(actual).Len()
matcher.Matches = func(actual A) bool {
lenOfActual := len(actual)
matcher.Actual = fmt.Sprintf("length was %d", lenOfActual)
switch expected.(type) {
case *gocrest.Matcher:
m := expected.(*gocrest.Matcher)
matcher.Describe = fmt.Sprintf(description, m.Describe)
return m.Matches(lenOfActual)
}
return lenOfActual == expected
}
return matcher
}

// MapLength can be called with maps
// Returns a matcher that matches if the length matches the given criteria
func MapLength[K comparable, V any](expected int) *gocrest.Matcher[map[K]V] {
const description = "value with length %v"
matcher := new(gocrest.Matcher[map[K]V])
matcher.Describe = fmt.Sprintf(description, expected)
matcher.Matches = func(actual map[K]V) bool {
lenOfActual := len(actual)
matcher.Actual = fmt.Sprintf("length was %d", lenOfActual)
return lenOfActual == expected
}
return matcher
}

// LengthMatching can be called with arrays or strings
// Returns a matcher that matches if the length matches matcher passed in
func LengthMatching[V any, A []V | string](expected *gocrest.Matcher[int]) *gocrest.Matcher[A] {
const description = "value with length %v"
matcher := new(gocrest.Matcher[A])
matcher.Describe = fmt.Sprintf(description, expected)
matcher.Matches = func(actual A) bool {
lenOfActual := len(actual)
matcher.Actual = fmt.Sprintf("length was %d", lenOfActual)
return expected.Matches(lenOfActual)

}
return matcher
}

// MapLengthMatching can be called with maps
// Returns a matcher that matches if the length matches the given matcher
func MapLengthMatching[K comparable, V any](expected *gocrest.Matcher[int]) *gocrest.Matcher[map[K]V] {
const description = "value with length %v"
matcher := new(gocrest.Matcher[map[K]V])
matcher.Describe = fmt.Sprintf(description, expected)
matcher.Matches = func(actual map[K]V) bool {
lenOfActual := len(actual)
matcher.Actual = fmt.Sprintf("length was %d", lenOfActual)
return expected.Matches(lenOfActual)

}
return matcher
}
9 changes: 4 additions & 5 deletions has/hasprefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import (
)

// Prefix returns a matcher that matches if the given string is prefixed with the expected string
// Function panics if the actual is not a string.
// Uses strings.Prefix(act, exp) to evaluate strings.
// Returns a matcher that returns true if the above conditions are met
func Prefix(expected string) *gocrest.Matcher {
matcher := new(gocrest.Matcher)
func Prefix(expected string) *gocrest.Matcher[string] {
matcher := new(gocrest.Matcher[string])
matcher.Describe = fmt.Sprintf("value with prefix %s", expected)
matcher.Matches = func(actual interface{}) bool {
return strings.HasPrefix(actual.(string), expected)
matcher.Matches = func(actual string) bool {
return strings.HasPrefix(actual, expected)
}
return matcher
}
Loading

0 comments on commit c340bb4

Please sign in to comment.