github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/elvish/tt/tt.go (about)

     1  // Package tt supports table-driven tests with little boilerplate.
     2  //
     3  // See the test case for this package for example usage.
     4  package tt
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"reflect"
    10  )
    11  
    12  // Table represents a test table.
    13  type Table []*Case
    14  
    15  // Case represents a test case. It is created by the C function, and offers
    16  // setters that augment and return itself; those calls can be chained like
    17  // C(...).Rets(...).
    18  type Case struct {
    19  	args         []interface{}
    20  	retsMatchers [][]interface{}
    21  }
    22  
    23  // Args returns a new Case with the given arguments.
    24  func Args(args ...interface{}) *Case {
    25  	return &Case{args: args}
    26  }
    27  
    28  // Rets modifies the test case so that it requires the return values to match
    29  // the given values. It returns the receiver. The arguments may implement the
    30  // Matcher interface, in which case its Match method is called with the actual
    31  // return value. Otherwise, reflect.DeepEqual is used to determine matches.
    32  func (c *Case) Rets(matchers ...interface{}) *Case {
    33  	c.retsMatchers = append(c.retsMatchers, matchers)
    34  	return c
    35  }
    36  
    37  // FnToTest describes a function to test.
    38  type FnToTest struct {
    39  	name    string
    40  	body    interface{}
    41  	argsFmt string
    42  	retsFmt string
    43  }
    44  
    45  // Fn makes a new FnToTest with the given function name and body.
    46  func Fn(name string, body interface{}) *FnToTest {
    47  	return &FnToTest{name: name, body: body}
    48  }
    49  
    50  // ArgsFmt sets the string for formatting arguments in test error messages, and
    51  // return fn itself.
    52  func (fn *FnToTest) ArgsFmt(s string) *FnToTest {
    53  	fn.argsFmt = s
    54  	return fn
    55  }
    56  
    57  // RetsFmt sets the string for formatting return values in test error messages,
    58  // and return fn itself.
    59  func (fn *FnToTest) RetsFmt(s string) *FnToTest {
    60  	fn.retsFmt = s
    61  	return fn
    62  }
    63  
    64  // T is the interface for accessing testing.T.
    65  type T interface {
    66  	Errorf(format string, args ...interface{})
    67  }
    68  
    69  // Test tests a function against test cases.
    70  func Test(t T, fn *FnToTest, tests Table) {
    71  	for _, test := range tests {
    72  		rets := call(fn.body, test.args)
    73  		for _, retsMatcher := range test.retsMatchers {
    74  			if !match(retsMatcher, rets) {
    75  				var argsString, retsString, wantRetsString string
    76  				if fn.argsFmt == "" {
    77  					argsString = sprintArgs(test.args...)
    78  				} else {
    79  					argsString = fmt.Sprintf(fn.argsFmt, test.args...)
    80  				}
    81  				if fn.retsFmt == "" {
    82  					retsString = sprintRets(rets...)
    83  					wantRetsString = sprintRets(retsMatcher...)
    84  				} else {
    85  					retsString = fmt.Sprintf(fn.retsFmt, rets...)
    86  					wantRetsString = fmt.Sprintf(fn.retsFmt, retsMatcher...)
    87  				}
    88  				t.Errorf("%s(%s) -> %s, want %s", fn.name, argsString, retsString, wantRetsString)
    89  			}
    90  		}
    91  	}
    92  }
    93  
    94  // RetValue is an empty interface used in the Matcher interface.
    95  type RetValue interface{}
    96  
    97  // Matcher wraps the Match method.
    98  type Matcher interface {
    99  	// Match reports whether a return value is considered a match. The argument
   100  	// is of type RetValue so that it cannot be implemented accidentally.
   101  	Match(RetValue) bool
   102  }
   103  
   104  func match(matchers, actual []interface{}) bool {
   105  	for i, matcher := range matchers {
   106  		if !matchOne(matcher, actual[i]) {
   107  			return false
   108  		}
   109  	}
   110  	return true
   111  }
   112  
   113  func matchOne(m, a interface{}) bool {
   114  	if m, ok := m.(Matcher); ok {
   115  		return m.Match(a)
   116  	}
   117  	return reflect.DeepEqual(m, a)
   118  }
   119  
   120  func sprintArgs(args ...interface{}) string {
   121  	return sprintCommaDelimited(args...)
   122  }
   123  
   124  func sprintRets(rets ...interface{}) string {
   125  	if len(rets) == 1 {
   126  		return fmt.Sprint(rets[0])
   127  	}
   128  	return "(" + sprintCommaDelimited(rets...) + ")"
   129  }
   130  
   131  func sprintCommaDelimited(args ...interface{}) string {
   132  	var b bytes.Buffer
   133  	for i, arg := range args {
   134  		if i > 0 {
   135  			b.WriteString(", ")
   136  		}
   137  		fmt.Fprint(&b, arg)
   138  	}
   139  	return b.String()
   140  }
   141  
   142  func call(fn interface{}, args []interface{}) []interface{} {
   143  	argsReflect := make([]reflect.Value, len(args))
   144  	for i, arg := range args {
   145  		argsReflect[i] = reflect.ValueOf(arg)
   146  	}
   147  	retsReflect := reflect.ValueOf(fn).Call(argsReflect)
   148  	rets := make([]interface{}, len(retsReflect))
   149  	for i, retReflect := range retsReflect {
   150  		rets[i] = retReflect.Interface()
   151  	}
   152  	return rets
   153  }