diff --git a/cmp_funcs_test.go b/cmp_funcs_test.go index 096a6bef..3d3d7ebd 100644 --- a/cmp_funcs_test.go +++ b/cmp_funcs_test.go @@ -10,6 +10,7 @@ package testdeep import ( "bytes" + "encoding/json" "errors" "fmt" "math" @@ -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() { diff --git a/example_test.go b/example_test.go index ff60a75f..d34fe0ac 100644 --- a/example_test.go +++ b/example_test.go @@ -8,6 +8,7 @@ package testdeep import ( "bytes" + "encoding/json" "errors" "fmt" "math" @@ -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() { diff --git a/t_test.go b/t_test.go index 7f9af685..7b8f7821 100644 --- a/t_test.go +++ b/t_test.go @@ -10,6 +10,7 @@ package testdeep import ( "bytes" + "encoding/json" "errors" "fmt" "math" @@ -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() { diff --git a/td_smuggle.go b/td_smuggle.go index b60b7ab9..3750aea6 100644 --- a/td_smuggle.go +++ b/td_smuggle.go @@ -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: @@ -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: // @@ -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, but you can act on it too -// by returning a SmuggledGot struct (by value or by address): +// something like DATA.MyTimeField, 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{ @@ -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 { @@ -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 @@ -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 } @@ -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 @@ -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 { diff --git a/td_smuggle_test.go b/td_smuggle_test.go index 48f5ae4b..4618f804 100644 --- a/td_smuggle_test.go +++ b/td_smuggle_test.go @@ -7,6 +7,7 @@ package testdeep_test import ( + "errors" "testing" "time" @@ -146,25 +147,54 @@ func TestSmuggle(t *testing.T) { }, testdeep.Contains("oob"))) + // + // Convertible types + checkOK(t, 123, + testdeep.Smuggle(func(n float64) int { return int(n) }, 123)) + + type xInt int + checkOK(t, xInt(123), + testdeep.Smuggle(func(n int) int64 { return int64(n) }, int64(123))) + checkOK(t, xInt(123), + testdeep.Smuggle(func(n uint32) int64 { return int64(n) }, int64(123))) + + type tVal struct{ Val interface{} } + checkOK(t, tVal{Val: int32(123)}, + testdeep.Struct(tVal{}, testdeep.StructFields{ + "Val": testdeep.Smuggle(func(n int64) int { return int(n) }, 123), + })) + // // Errors - checkError(t, 123, + checkError(t, "123", testdeep.Smuggle(func(n float64) int { return int(n) }, 123), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), - Got: mustBe("int"), + Got: mustBe("string"), Expected: mustBe("float64"), }) - type xInt int - checkError(t, xInt(12), - testdeep.Smuggle(func(n int) int64 { return int64(n) }, 12), + checkError(t, tVal{}, + testdeep.Struct(tVal{}, testdeep.StructFields{ + "Val": testdeep.Smuggle(func(n int64) int { return int(n) }, 123), + }), expectedError{ Message: mustBe("incompatible parameter type"), - Path: mustBe("DATA"), - Got: mustBe("testdeep_test.xInt"), - Expected: mustBe("int"), + Path: mustBe("DATA.Val"), + Got: mustBe("interface {}"), + Expected: mustBe("int64"), + }) + + checkError(t, tVal{Val: "str"}, + testdeep.Struct(tVal{}, testdeep.StructFields{ + "Val": testdeep.Smuggle(func(n int64) int { return int(n) }, 123), + }), + expectedError{ + Message: mustBe("incompatible parameter type"), + Path: mustBe("DATA.Val"), + Got: mustBe("string"), + Expected: mustBe("int64"), }) checkError(t, 12, @@ -187,6 +217,16 @@ func TestSmuggle(t *testing.T) { Summary: mustBe(" value: (int) 12\nit failed coz: very custom error"), }) + checkError(t, 12, + testdeep.Smuggle(func(n int) (int, error) { + return n, errors.New("very custom error") + }, 12), + expectedError{ + Message: mustBe("ran smuggle code with %% as argument"), + Path: mustBe("DATA"), + Summary: mustBe(" value: (int) 12\nit failed coz: very custom error"), + }) + checkError(t, 12, testdeep.Smuggle(func(n int) *testdeep.SmuggledGot { return nil }, int64(13)), expectedError{ @@ -263,24 +303,30 @@ func TestSmuggle(t *testing.T) { }, "FUNC must take only one argument") // Bad number of returned values + const errMesg = "FUNC must return value or (value, bool) or (value, bool, string) or (value, error)" + test.CheckPanic(t, func() { testdeep.Smuggle(func(a int) {}, 12) - }, "FUNC must return value or (value, bool) or (value, bool, string)") + }, errMesg) test.CheckPanic(t, func() { testdeep.Smuggle( func(a int) (int, bool, string, int) { return 0, false, "", 23 }, 12) - }, "FUNC must return value or (value, bool) or (value, bool, string)") + }, errMesg) // Bad returned types test.CheckPanic(t, func() { testdeep.Smuggle(func(a int) (int, int) { return 0, 0 }, 12) - }, "FUNC must return value or (value, bool) or (value, bool, string)") + }, errMesg) test.CheckPanic(t, func() { testdeep.Smuggle(func(a int) (int, bool, int) { return 0, false, 23 }, 12) - }, "FUNC must return value or (value, bool) or (value, bool, string)") + }, errMesg) + + test.CheckPanic(t, func() { + testdeep.Smuggle(func(a int) (int, error, string) { return 0, nil, "" }, 12) + }, errMesg) // // String @@ -292,6 +338,10 @@ func TestSmuggle(t *testing.T) { testdeep.Smuggle(func(n int) (int, bool) { return 23, false }, 12).String(), "Smuggle(func(int) (int, bool))") + test.EqualStr(t, + testdeep.Smuggle(func(n int) (int, error) { return 23, nil }, 12).String(), + "Smuggle(func(int) (int, error))") + test.EqualStr(t, testdeep.Smuggle(func(n int) (int, MyBool, MyString) { return 23, false, "" }, 12). String(), diff --git a/types.go b/types.go index fcc6fa02..9f10f1f1 100644 --- a/types.go +++ b/types.go @@ -21,6 +21,7 @@ import ( var ( testDeeper = reflect.TypeOf((*TestDeep)(nil)).Elem() stringerInterface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() + errorInterface = reflect.TypeOf((*error)(nil)).Elem() timeType = reflect.TypeOf(time.Time{}) intType = reflect.TypeOf(int(0)) smuggledGotType = reflect.TypeOf(SmuggledGot{})