Skip to content

Commit

Permalink
Fix some bugs related to d.Func
Browse files Browse the repository at this point in the history
If the func takes an interface, we need to check that the argument implements
the interface, as opposed to just checking that the types are equal.

Fixed calling d.Is() or other assertion methods inside a func passed to
d.Func.
  • Loading branch information
autarch committed Mar 27, 2021
1 parent 5ed9b3d commit 7d5c1ee
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 18 deletions.
9 changes: 9 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.0.5 2021-03-27

* Fix handling of functions passed to `d.Func` which take an interface as an
argument. No matter what was passed to them detest would treat it as an
error because the types didn't match.
* Make it possible to call additional detest assertion methods, like `d.Is`,
inside a func passed to `d.Func`.


## 0.0.4 2021-03-26

* Even more `nil` fixes. The fix in the last release for passing `nil` to a
Expand Down
24 changes: 19 additions & 5 deletions pkg/detest/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,30 @@ func (d *D) newFunc(with interface{}, name, called string) (FuncComparer, error)
// `d.Actual()`. The function is expected to return a boolean indicating
// success or failure.
func (fc FuncComparer) Compare(d *D) {
v := reflect.ValueOf(d.Actual())
actual := d.Actual()
v := reflect.ValueOf(actual)

d.PushPath(d.NewPath(describeTypeOfReflectValue(v), 1, fc.name))
defer d.PopPath()

inType := fc.comparer.Type().In(0)
if (v.IsValid() && v.Type() != inType) ||
(!v.IsValid() && !isNilable(inType.Kind())) {
okInput := false
if v.IsValid() {
// Either the types are the same or the input implements the input
// type is an interface and the value implements it.
if v.Type() == inType ||
(inType.Kind() == reflect.Interface && v.Type().Implements(inType)) {
okInput = true
}
} else {
if isNilable(inType.Kind()) {
okInput = true
}
}

if !okInput {
d.AddResult(result{
actual: newValue(d.Actual()),
actual: newValue(actual),
pass: false,
op: "func()",
where: inUsage,
Expand Down Expand Up @@ -108,7 +122,7 @@ func (fc FuncComparer) Compare(d *D) {
}
ret := fc.comparer.Call([]reflect.Value{v})
r := result{
actual: newValue(d.Actual()),
actual: newValue(actual),
pass: ret[0].Bool(),
op: "func()",
}
Expand Down
133 changes: 124 additions & 9 deletions pkg/detest/func_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package detest

import (
"errors"
"fmt"
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -21,8 +23,10 @@ func TestFunc(t *testing.T) {
{"Func with name fails with description", funcWithNameFailsWithDescription},
{"Func cannot accept the given argument", funcCannotAcceptArgument},
{"Func creation errors", funcCreationErrors},
{"Func input type is interface", funcInputTypeIsInterface},
{"Func handles untyped nil", funcHandlesUntypedNil},
{"Func handles typed nil", funcHandlesTypedNil},
{"Func call additional detest methods", funcCallsAdditionalDetestMethods},
}

for _, test := range tests {
Expand All @@ -36,7 +40,7 @@ func funcWithNoNamePasses(t *testing.T) {
f, err := d.Func(func(s []int) bool {
return len(s) < 4
})
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
d.Passes(
[]int{1, 2, 3},
f,
Expand All @@ -52,7 +56,7 @@ func funcWithNamePasses(t *testing.T) {
f, err := d.NamedFunc(func(s []int) bool {
return len(s) < 4
}, "Has a name")
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
d.Passes(
[]int{1, 2, 3},
f,
Expand All @@ -68,7 +72,7 @@ func funcWithNoNameFails(t *testing.T) {
f, err := d.Func(func(s []int) bool {
return len(s) < 4
})
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
[]int{1, 2, 3, 4},
Expand Down Expand Up @@ -106,7 +110,7 @@ func funcWithNameFails(t *testing.T) {
f, err := d.NamedFunc(func(s []int) bool {
return len(s) < 4
}, "Has a name")
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
[]int{1, 2, 3, 4},
Expand Down Expand Up @@ -144,7 +148,7 @@ func funcWithNoNameFailsWithDescription(t *testing.T) {
f, err := d.Func(func(s []int) (bool, string) {
return len(s) < 4, fmt.Sprintf("Slice is %d elements long but cannot be more than 3", len(s))
})
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
[]int{1, 2, 3, 4},
Expand Down Expand Up @@ -182,7 +186,7 @@ func funcWithNameFailsWithDescription(t *testing.T) {
f, err := d.NamedFunc(func(s []int) (bool, string) {
return len(s) < 4, fmt.Sprintf("Slice is %d elements long but cannot be more than 3", len(s))
}, "Has a name")
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
[]int{1, 2, 3, 4},
Expand Down Expand Up @@ -220,7 +224,7 @@ func funcCannotAcceptArgument(t *testing.T) {
f, err := d.NamedFunc(func(s []int) bool {
return len(s) < 4
}, "Has a name")
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
42,
Expand Down Expand Up @@ -313,13 +317,60 @@ func funcCreationErrors(t *testing.T) {
)
}

func funcInputTypeIsInterface(t *testing.T) {
t.Run("error interface input with wrong error", func(t *testing.T) {
mockT := new(mockT)
d := NewWithOutput(mockT, mockT)
f, err := d.Func(func(err error) bool {
return err != nil && errors.Is(err, os.ErrNotExist)
})
require.NoError(t, err, "no error calling Func()")
d.Passes(
errors.New("foo"),
f,
"can pass concrete value to func that takes interface",
)
mockT.AssertCalled(t, "Fail")
})

t.Run("error interface input with right error", func(t *testing.T) {
mockT := new(mockT)
d := NewWithOutput(mockT, mockT)
f, err := d.Func(func(err error) bool {
return err != nil && errors.Is(err, os.ErrNotExist)
})
require.NoError(t, err, "no error calling Func()")
d.Passes(
os.ErrNotExist,
f,
"can pass concrete value to func that takes interface",
)
mockT.AssertNotCalled(t, "Fail")
})

t.Run("error interface input with wrong type", func(t *testing.T) {
mockT := new(mockT)
d := NewWithOutput(mockT, mockT)
f, err := d.Func(func(err error) bool {
return err != nil && errors.Is(err, os.ErrNotExist)
})
require.NoError(t, err, "no error calling Func()")
d.Passes(
42,
f,
"can pass wrong type to func that takes interface",
)
mockT.AssertCalled(t, "Fail")
})
}

func funcHandlesUntypedNil(t *testing.T) {
mockT := new(mockT)
d := NewWithOutput(mockT, mockT)
f, err := d.Func(func(s []int) bool {
return s != nil && len(s) < 4
})
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
r.Passes(
nil,
Expand Down Expand Up @@ -357,7 +408,7 @@ func funcHandlesTypedNil(t *testing.T) {
f, err := d.Func(func(s []int) bool {
return s != nil && len(s) < 4
})
assert.NoError(t, err, "no error calling Func()")
require.NoError(t, err, "no error calling Func()")
r := NewRecorder(d)
var s []int
r.Passes(
Expand Down Expand Up @@ -389,3 +440,67 @@ func funcHandlesTypedNil(t *testing.T) {
"got the expected result",
)
}

type myError struct {
size int
}

func (m myError) Error() string {
return "foo"
}

func funcCallsAdditionalDetestMethods(t *testing.T) {
mockT := new(mockT)
d := NewWithOutput(mockT, mockT)
r := NewRecorder(d)
f, err := r.Func(func(err error) bool {
var realErr myError
return r.Is(errors.As(err, &realErr), true) && r.Is(realErr.size, 42)
})
require.NoError(t, err, "no error calling Func()")
e := myError{size: 42}
r.Passes(
e,
f,
"myError 42",
)
mockT.AssertNotCalled(t, "Fail")
require.Len(t, r.record, 3, "three states were recorded")
assert.Len(t, r.record[0].output, 1, "record has state with one output item")
assert.Equal(
t,
&result{
actual: &value{value: true, desc: ""},
expect: &value{value: true, desc: ""},
op: "==",
pass: true,
path: []Path{
{
data: "bool",
callee: "detest.(*D).Equal",
caller: "detest.(*DetestRecorder).Is",
},
},
},
r.record[0].output[0].result,
"got the expected result for first assertion",
)
assert.Equal(
t,
&result{
actual: &value{value: 42, desc: ""},
expect: &value{value: 42, desc: ""},
op: "==",
pass: true,
path: []Path{
{
data: "int",
callee: "detest.(*D).Equal",
caller: "detest.(*DetestRecorder).Is",
},
},
},
r.record[1].output[0].result,
"got the expected result for second assertion",
)
}
8 changes: 4 additions & 4 deletions pkg/detest/shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ func NewRecorder(d *D) *DetestRecorder {
}
}

func (d *DetestRecorder) Is(actual, expect interface{}, name string) bool {
ok := d.D.Is(actual, expect, name)
func (d *DetestRecorder) Is(actual, expect interface{}, args ...interface{}) bool {
ok := d.D.Is(actual, expect, args...)
d.record = append(d.record, d.D.state)
return ok
}

func (d *DetestRecorder) Passes(actual, expect interface{}, name string) bool {
ok := d.D.Passes(actual, expect.(Comparer), name)
func (d *DetestRecorder) Passes(actual, expect interface{}, args ...interface{}) bool {
ok := d.D.Passes(actual, expect.(Comparer), args...)
d.record = append(d.record, d.D.state)
return ok
}
Expand Down

0 comments on commit 7d5c1ee

Please sign in to comment.