Skip to content

Commit

Permalink
Add Smuggle operator
Browse files Browse the repository at this point in the history
Signed-off-by: Maxime Soulé <btik-git@scoubidou.com>
  • Loading branch information
maxatome committed Jun 15, 2018
1 parent 2bed1c6 commit 92caf28
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 14 deletions.
8 changes: 7 additions & 1 deletion td_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type tdCode struct {

var _ TestDeep = &tdCode{}

// Code operator allows to checks data using a custom function. So
// Code operator allows to check data using a custom function. So
// "fn" is a function that must take one parameter whose type must be
// the same as the type of the compared value.
//
Expand All @@ -41,6 +41,12 @@ var _ TestDeep = &tdCode{}
//
// This operator allows to handle any specific comparison not handled
// by standard operators.
//
// It is not recommended to call CmpDeeply (or any other Cmp*
// functions or *T methods) inside the body of "fn", because of
// confusion produced by output in case of failure. When the data
// needs to be tranformed before being compared again, Smuggle
// operator should be used instead.
func Code(fn interface{}) TestDeep {
vfn := reflect.ValueOf(fn)

Expand Down
4 changes: 2 additions & 2 deletions td_len_cap.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
)

type tdLenCapBase struct {
tdSmuggler
tdSmugglerBase
}

func (b *tdLenCapBase) initLenCapBase(val interface{}) bool {
vval := reflect.ValueOf(val)
if vval.IsValid() {
b.tdSmuggler = newSmuggler(val, 5)
b.tdSmugglerBase = newSmugglerBase(val, 5)

if b.isTestDeeper {
return true
Expand Down
8 changes: 4 additions & 4 deletions td_ptr.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

type tdPtr struct {
tdSmuggler
tdSmugglerBase
}

var _ TestDeep = &tdPtr{}
Expand All @@ -28,7 +28,7 @@ func Ptr(val interface{}) TestDeep {
vval := reflect.ValueOf(val)
if vval.IsValid() {
p := tdPtr{
tdSmuggler: newSmuggler(val),
tdSmugglerBase: newSmugglerBase(val),
}

if !p.isTestDeeper {
Expand Down Expand Up @@ -66,7 +66,7 @@ func (p *tdPtr) String() string {
}

type tdPPtr struct {
tdSmuggler
tdSmugglerBase
}

var _ TestDeep = &tdPPtr{}
Expand All @@ -86,7 +86,7 @@ func PPtr(val interface{}) TestDeep {
vval := reflect.ValueOf(val)
if vval.IsValid() {
p := tdPPtr{
tdSmuggler: newSmuggler(val),
tdSmugglerBase: newSmugglerBase(val),
}

if !p.isTestDeeper {
Expand Down
244 changes: 244 additions & 0 deletions td_smuggle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Copyright (c) 2018, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package testdeep

import (
"reflect"
"unicode"
"unicode/utf8"
)

type SmuggledGot struct {
Name string
Got interface{}
}

const smuggled = "<smuggled>"

func (s SmuggledGot) contextAndGot(ctx Context) (Context, reflect.Value) {
// If the Name starts with a Letter, prefix it by a "."
var name string
if s.Name != "" {
first, size := utf8.DecodeRuneInString(s.Name)
if size != 0 && unicode.IsLetter(first) {
name = "."
}
name += s.Name
} else {
name = smuggled
}
return ctx.AddDepth(name), reflect.ValueOf(s.Got)
}

type tdSmuggle struct {
tdSmugglerBase
function reflect.Value
argType reflect.Type
}

var _ TestDeep = &tdSmuggle{}

// Smuggle operator allows to change data contents or mutate it into
// another type. So "fn" is a function that must take one parameter
// whose type must be the same as 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:
//
// Smuggle(func (value string) int {
// num, _ := strconv.Atoi(value)
// return num
// },
// 28)
//
// or using an other TestDeep operator:
//
// Smuggle(func (value string) int {
// num, _ := strconv.Atoi(value)
// return num
// },
// Between(28, 30))
//
// "fn" can return a second boolean value, used to tell that a problem
// occurred and so stop the comparison:
//
// Smuggle(func (value string) (int, bool) {
// num, err := strconv.Atoi(value)
// return num, err == nil
// },
// Between(28, 30))
//
// "fn" can return a third string value which is used to describe the
// test when a problem occurred (false second boolean value):
//
// Smuggle(func (value string) (int, bool) {
// num, err := strconv.Atoi(value)
// if err != nil {
// return num, false, "string must contain a number"
// }
// return num, true, ""
// },
// Between(28, 30))
//
// Imagine you want to compare that the Year of a date is between 2010
// and 2020:
//
// Smuggle(func (date time.Time) int {
// return date.Year()
// },
// 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):
//
// Smuggle(func (date time.Time) SmuggledGot {
// return SmuggledGot{
// Name: "Year",
// Got: date.Year(),
// }
// },
// Between(2010, 2020))
//
// then the data location forwarded to next test will be somthing like
// DATA.MyTimeField.Year. The "." between the current path (here
// "DATA.MyTimeField") and the returned Name "Year" is automatically
// added when Name starts with a Letter.
//
// Note that SmuggledGot and *SmuggledGot returns are treated equally,
// and they are only used when "fn" has only one returned value or
// when the second boolean returned value is true.
//
// Of course, all cases can go together:
//
// // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and
// // tests whether this date is contained between NOW less 2 hours and NOW.
// Smuggle(func (date string) (*SmuggledGot, bool, string) {
// date, err := time.Parse("2006/01/02 15:04:05", date)
// if err != nil {
// return nil, false, `date must conform to "YYYY/mm/DD HH:MM:SS" format`
// }
// return &SmuggledGot{
// Name: "Date",
// Got: date,
// }, true, ""
// },
// 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.
func Smuggle(fn interface{}, expectedValue interface{}) TestDeep {
vfn := reflect.ValueOf(fn)

const usage = "Smuggle(FUNC, TESTDEEP_OPERATOR|EXPECTED_VALUE)"

if vfn.Kind() != reflect.Func {
panic("usage: " + usage)
}

fnType := vfn.Type()
if fnType.NumIn() != 1 {
panic(usage + ": FUNC must take only one argument")
}

switch fnType.NumOut() {
case 3: // (value, bool, string)
if fnType.Out(2).Kind() != reflect.String {
break
}
fallthrough

case 2: // (value, bool)
if fnType.Out(1).Kind() != reflect.Bool {
break
}
fallthrough

case 1: // (value)
return &tdSmuggle{
tdSmugglerBase: newSmugglerBase(expectedValue),
function: vfn,
argType: fnType.In(0),
}
}

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

func (s *tdSmuggle) Match(ctx Context, got reflect.Value) *Error {
if !got.Type().AssignableTo(s.argType) {
if ctx.booleanError {
return booleanError
}
return ctx.CollectError(&Error{
Message: "incompatible parameter type",
Got: rawString(got.Type().String()),
Expected: rawString(s.argType.String()),
})
}

// Refuse to override unexported fields access in this case. It is a
// choice, as we think it is better to work on surrounding struct
// instead.
if !got.CanInterface() {
if ctx.booleanError {
return booleanError
}
return ctx.CollectError(&Error{
Message: "cannot smuggle unexported field",
Summary: rawString("work on surrounding struct instead"),
})
}

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

var newCtx Context
if newGot.IsValid() {
switch newGot.Type() {
case smuggledGotType:
newCtx, newGot = newGot.Interface().(SmuggledGot).contextAndGot(ctx)

case smuggledGotPtrType:
newCtx, newGot = newGot.Interface().(*SmuggledGot).contextAndGot(ctx)

default:
newCtx = ctx.AddDepth(smuggled)
}
}

return deepValueEqual(newCtx, newGot, s.expectedValue)
}

if ctx.booleanError {
return booleanError
}

err := Error{
Message: "ran smuggle code with %% as argument",
}

if len(ret) > 2 {
err.Summary = tdCodeResult{
Value: got,
Reason: ret[2].String(),
}
} else {
err.Summary = tdCodeResult{
Value: got,
}
}

return ctx.CollectError(&err)
}

func (s *tdSmuggle) String() string {
return "Smuggle(" + s.function.Type().String() + ")"
}
Loading

0 comments on commit 92caf28

Please sign in to comment.