Skip to content
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

Smuggle operator more lax + func can return error #32

Merged
merged 1 commit into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cmp_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package testdeep

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -1635,10 +1636,55 @@ func ExampleCmpSmuggle_convert() {
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

ok = CmpSmuggle(t, "123", func(numStr string) (int, error) {
return strconv.Atoi(numStr)
}, Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Short version :)
ok = CmpSmuggle(t, "123", strconv.Atoi, Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Output:
// true
// true
// true
// true
// true
}

func ExampleCmpSmuggle_lax() {
t := &testing.T{}

// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)

ok := CmpSmuggle(t, got, func(n int64) uint32 { return uint32(n) }, uint32(123))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)

// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}

func ExampleCmpSmuggle_auto_unmarshal() {
t := &testing.T{}

// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)

ok := CmpSmuggle(t, got, func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
}, map[string]int{
"a": 1,
"b": 2,
})
fmt.Println("JSON contents is OK:", ok)

// Output:
// JSON contents is OK: true
}

func ExampleCmpSmuggle_complex() {
Expand Down
54 changes: 54 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package testdeep

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -1713,10 +1714,63 @@ func ExampleSmuggle_convert() {
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

ok = CmpDeeply(t, "123",
Smuggle(
func(numStr string) (int, error) {
return strconv.Atoi(numStr)
},
Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Short version :)
ok = CmpDeeply(t, "123",
Smuggle(strconv.Atoi, Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Output:
// true
// true
// true
// true
// true
}

func ExampleSmuggle_lax() {
t := &testing.T{}

// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)

ok := CmpDeeply(t, got,
Smuggle(func(n int64) uint32 { return uint32(n) }, uint32(123)))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)

// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}

func ExampleSmuggle_auto_unmarshal() {
t := &testing.T{}

// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)

ok := CmpDeeply(t, got,
Smuggle(
func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
},
map[string]int{
"a": 1,
"b": 2,
}))
fmt.Println("JSON contents is OK:", ok)

// Output:
// JSON contents is OK: true
}

func ExampleSmuggle_complex() {
Expand Down
46 changes: 46 additions & 0 deletions t_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package testdeep

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -1635,10 +1636,55 @@ func ExampleT_Smuggle_convert() {
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

ok = t.Smuggle("123", func(numStr string) (int, error) {
return strconv.Atoi(numStr)
}, Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Short version :)
ok = t.Smuggle("123", strconv.Atoi, Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)

// Output:
// true
// true
// true
// true
// true
}

func ExampleT_Smuggle_lax() {
t := NewT(&testing.T{})

// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)

ok := t.Smuggle(got, func(n int64) uint32 { return uint32(n) }, uint32(123))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)

// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}

func ExampleT_Smuggle_auto_unmarshal() {
t := NewT(&testing.T{})

// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)

ok := t.Smuggle(got, func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
}, map[string]int{
"a": 1,
"b": 2,
})
fmt.Println("JSON contents is OK:", ok)

// Output:
// JSON contents is OK: true
}

func ExampleT_Smuggle_complex() {
Expand Down
100 changes: 79 additions & 21 deletions td_smuggle.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var _ TestDeep = &tdSmuggle{}
// Smuggle operator allows to change data contents or mutate it into
// another type before stepping down in favor of generic comparison
// process. So "fn" is a function that must take one parameter whose
// type must be the same as the type of the compared value.
// type must be convertible to the type of the compared value.
//
// "fn" must return at least one value, these value will be compared as is
// to "expectedValue", here integer 28:
Expand Down Expand Up @@ -90,6 +90,20 @@ var _ TestDeep = &tdSmuggle{}
// },
// Between(28, 30))
//
// Instead of returning (X, bool) or (X, bool, string), "fn" can
// return (X, error). When a problem occurs, the returned error is
// non-nil, as in:
//
// Smuggle(func (value string) (int, error) {
// num, err := strconv.Atoi(value)
// return num, err
// },
// Between(28, 30))
//
// Which can be simplified to:
//
// Smuggle(strconv.Atoi, Between(28, 30))
//
// Imagine you want to compare that the Year of a date is between 2010
// and 2020:
//
Expand All @@ -99,8 +113,8 @@ var _ TestDeep = &tdSmuggle{}
// Between(2010, 2020))
//
// In this case the data location forwarded to next test will be
// somthing like DATA.MyTimeField<smuggled>, but you can act on it too
// by returning a SmuggledGot struct (by value or by address):
// something like DATA.MyTimeField<smuggled>, but you can act on it
// too by returning a SmuggledGot struct (by value or by address):
//
// Smuggle(func (date time.Time) SmuggledGot {
// return SmuggledGot{
Expand Down Expand Up @@ -135,9 +149,29 @@ var _ TestDeep = &tdSmuggle{}
// },
// Between(time.Now().Add(-2*time.Hour), time.Now()))
//
// or:
//
// // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and tests
// // whether this date is contained between 2 hours before now and now.
// Smuggle(func (date string) (*SmuggledGot, error) {
// date, err := time.Parse("2006/01/02 15:04:05", date)
// if err != nil {
// return nil, err
// }
// return &SmuggledGot{
// Name: "Date",
// Got: date,
// }, nil
// },
// Between(time.Now().Add(-2*time.Hour), time.Now()))
//
// The difference between Smuggle and Code operators is that Code is
// used to do a final comparison while Smuggle transforms the data and
// then steps down in favor of generic comparison process.
// then steps down in favor of generic comparison process. Moreover,
// the type accepted as input for the function is lax to facilitate
// the tests writing (eg. the function can accept an float64 and the
// got value be an int). See examples. On the other hand, the output
// type is strict and must match exactly the expected value type.
//
// TypeBehind method returns the reflect.Type of only parameter of "fn".
func Smuggle(fn interface{}, expectedValue interface{}) TestDeep {
Expand All @@ -161,8 +195,12 @@ func Smuggle(fn interface{}, expectedValue interface{}) TestDeep {
}
fallthrough

case 2: // (value, bool)
if fnType.Out(1).Kind() != reflect.Bool {
case 2:
// (value, *bool*) or (value, *bool*, string)
if fnType.Out(1).Kind() != reflect.Bool &&
// (value, *error*)
(fnType.NumOut() > 2 ||
fnType.Out(1) != errorInterface) {
break
}
fallthrough
Expand All @@ -180,11 +218,27 @@ func Smuggle(fn interface{}, expectedValue interface{}) TestDeep {
}

panic(usage +
": FUNC must return value or (value, bool) or (value, bool, string)")
": FUNC must return value or (value, bool) or (value, bool, string) or (value, error)")
}

func (s *tdSmuggle) laxConvert(got reflect.Value) (reflect.Value, bool) {
if !got.Type().ConvertibleTo(s.argType) {
if got.Kind() != reflect.Interface || got.IsNil() {
return got, false
}

got = got.Elem()
if !got.Type().ConvertibleTo(s.argType) {
return got, false
}
}

return got.Convert(s.argType), true
}

func (s *tdSmuggle) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if !got.Type().AssignableTo(s.argType) {
got, ok := s.laxConvert(got)
if !ok {
if ctx.BooleanError {
return ctxerr.BooleanError
}
Expand All @@ -209,7 +263,9 @@ func (s *tdSmuggle) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
}

ret := s.function.Call([]reflect.Value{got})
if len(ret) == 1 || ret[1].Bool() {
if len(ret) == 1 ||
(ret[1].Kind() == reflect.Bool && ret[1].Bool()) ||
(ret[1].Kind() == reflect.Interface && ret[1].IsNil()) {
newGot := ret[0]

var newCtx ctxerr.Context
Expand All @@ -236,22 +292,24 @@ func (s *tdSmuggle) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
return ctxerr.BooleanError
}

err := ctxerr.Error{
Message: "ran smuggle code with %% as argument",
summary := tdCodeResult{
Value: got,
}

if len(ret) > 2 {
err.Summary = tdCodeResult{
Value: got,
Reason: ret[2].String(),
}
} else {
err.Summary = tdCodeResult{
Value: got,
switch len(ret) {
case 3: // (value, false, string)
summary.Reason = ret[2].String()
case 2:
// (value, error)
if ret[1].Kind() == reflect.Interface {
summary.Reason = ret[1].Interface().(error).Error()
}
// (value, false)
}

return ctx.CollectError(&err)
return ctx.CollectError(&ctxerr.Error{
Message: "ran smuggle code with %% as argument",
Summary: summary,
})
}

func (s *tdSmuggle) String() string {
Expand Down
Loading