github.com/camlistore/go4@v0.0.0-20200104003542-c7e774b10ea0/testing/functest/functest.go (about)

     1  /*
     2  Copyright 2016 The go4.org Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package functest contains utilities to ease writing table-driven
    18  // tests for pure functions and method.
    19  //
    20  // Example:
    21  //
    22  //	func square(v int) int { return v * v }
    23  //
    24  //	func TestFunc(t *testing.T) {
    25  //		f := functest.New(square)
    26  //		f.Test(t,
    27  //			f.In(0).Want(0),
    28  //			f.In(1).Want(1),
    29  //			f.In(2).Want(4),
    30  //			f.In(3).Want(9),
    31  //		)
    32  //	}
    33  //
    34  // It can test whether things panic:
    35  //
    36  //	f := functest.New(condPanic)
    37  //	f.Test(t,
    38  //		f.In(false, nil),
    39  //		f.In(true, "boom").Check(func(res functest.Result) error {
    40  //			if res.Panic != "boom" {
    41  //				return fmt.Errorf("panic = %v; want boom", res.Panic)
    42  //			}
    43  //			return nil
    44  //		}),
    45  //		f.In(true, nil).Check(func(res functest.Result) error {
    46  //			if res.Panic != nil || res.Paniked {
    47  //				return fmt.Errorf("expected panic with nil value, got: %+v", res)
    48  //			}
    49  //			return nil
    50  //		}),
    51  //	)
    52  //
    53  // If a test fails, functest does its best to format a useful error message. You can also
    54  // name test cases:
    55  //
    56  //		f := functest.New(square)
    57  //		f.Test(t,
    58  //			f.In(0).Want(0),
    59  //			f.In(1).Want(111),
    60  //			f.In(2).Want(4),
    61  //			f.Case("three").In(3).Want(999),
    62  //		)
    63  //
    64  // Which would fail like:
    65  //
    66  //	--- FAIL: TestSquare (0.00s)
    67  //	functest.go:304: square(1) = 1; want 111
    68  //	functest.go:304: three: square(3) = 9; want 999
    69  //	FAIL
    70  //
    71  package functest
    72  
    73  import (
    74  	"bytes"
    75  	"fmt"
    76  	"reflect"
    77  	"runtime"
    78  	"strings"
    79  	"testing"
    80  )
    81  
    82  // Func is a wrapper around a func to test.
    83  // It must be created with New.
    84  type Func struct {
    85  	// Name is the name of the function to use in error messages.
    86  	// In most cases it is initialized by New, unless the function
    87  	// being tested is an anonymous function.
    88  	Name string
    89  
    90  	f  interface{}   // the func
    91  	fv reflect.Value // of f
    92  }
    93  
    94  var removePunc = strings.NewReplacer("(", "", ")", "", "*", "")
    95  
    96  // New wraps a function for testing.
    97  // The provided value f must be a function or method.
    98  func New(f interface{}) *Func {
    99  	fv := reflect.ValueOf(f)
   100  	if fv.Kind() != reflect.Func {
   101  		panic("argument to New must be a func")
   102  	}
   103  	var name string
   104  	rf := runtime.FuncForPC(fv.Pointer())
   105  	if rf != nil {
   106  		name = rf.Name()
   107  		if methType := strings.LastIndex(name, ".("); methType != -1 {
   108  			name = removePunc.Replace(name[methType+2:])
   109  		} else if lastDot := strings.LastIndex(name, "."); lastDot != -1 {
   110  			name = name[lastDot+1:]
   111  			if strings.HasPrefix(name, "func") {
   112  				// Looks like some anonymous function. Prefer naming it "f".
   113  				name = "f"
   114  			}
   115  		}
   116  	} else {
   117  		name = "f"
   118  	}
   119  
   120  	return &Func{
   121  		f:    f,
   122  		fv:   fv,
   123  		Name: name,
   124  	}
   125  }
   126  
   127  // Result is the result of a function call, for use with Check.
   128  type Result struct {
   129  	// Result is the return value(s) of the function.
   130  	Result []interface{}
   131  
   132  	// Panic is the panic value of the function.
   133  	Panic interface{}
   134  
   135  	// Panicked is whether the function paniced.
   136  	// It can be used to determine whether a function
   137  	// called panic(nil).
   138  	Panicked bool
   139  }
   140  
   141  // Case is a test case to run.
   142  //
   143  // Test cases can be either named or unnamed, depending on how they're
   144  // created. Naming cases is optional; all failures messages aim to
   145  // have useful output and include the input to the function.
   146  //
   147  // Unless the function's arity is zero, all cases should have their input
   148  // set with In.
   149  //
   150  // The case's expected output can be set with Want and/or Check.
   151  type Case struct {
   152  	f        *Func
   153  	in       []interface{}
   154  	name     string        // optional
   155  	want     []interface{} // non-nil if we check args
   156  	checkRes []func(Result) error
   157  }
   158  
   159  // Case returns a new named case. It should be modified before use.
   160  func (f *Func) Case(name string) *Case {
   161  	return &Case{f: f, name: name}
   162  }
   163  
   164  // In returns a new unnamed test case. It will be identified by its arguments
   165  // only.
   166  func (f *Func) In(args ...interface{}) *Case {
   167  	return &Case{f: f, in: args}
   168  }
   169  
   170  // In sets the arguments of c used to call f.
   171  func (c *Case) In(args ...interface{}) *Case {
   172  	c.in = args
   173  	return c
   174  }
   175  
   176  // Want sets the expected result values of the test case.
   177  // Want modifies and returns c.
   178  // Callers my use both Want and Check.
   179  func (c *Case) Want(result ...interface{}) *Case {
   180  	if c.want != nil {
   181  		panic("duplicate Want declared on functest.Case")
   182  	}
   183  	c.want = result
   184  	numOut := c.f.fv.Type().NumOut()
   185  	if len(result) != numOut {
   186  		// TODO: let caller providing only interesting result values, or
   187  		// provide matchers.
   188  		panic(fmt.Sprintf("Want called with %d values; function returns %d values", len(result), numOut))
   189  	}
   190  	return c
   191  }
   192  
   193  // Check adds a function to check the result of the case's function
   194  // call. It is a low-level function when Want is insufficient.
   195  // For instance, it allows checking whether a function panics.
   196  // If no checker functions are registered, function panics are considered
   197  // a test failure.
   198  //
   199  // Check modifies and returns c.
   200  // Callers my use both Want and Check, and may use Check multiple times.
   201  func (c *Case) Check(checker func(Result) error) *Case {
   202  	c.checkRes = append(c.checkRes, checker)
   203  	return c
   204  }
   205  
   206  // Test runs the provided test cases against f.
   207  // If any test cases fail, t.Errorf is called.
   208  func (f *Func) Test(t testing.TB, cases ...*Case) {
   209  	for _, tc := range cases {
   210  		f.testCase(t, tc)
   211  	}
   212  }
   213  
   214  func (f *Func) checkCall(in []reflect.Value) (out []reflect.Value, didPanic bool, panicValue interface{}) {
   215  	defer func() { panicValue = recover() }()
   216  	didPanic = true
   217  	out = f.fv.Call(in)
   218  	didPanic = false
   219  	return
   220  }
   221  
   222  var nilEmptyInterface = reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem())
   223  
   224  func (f *Func) testCase(t testing.TB, c *Case) {
   225  	// Non-variadic:
   226  	ft := f.fv.Type()
   227  	inReg := ft.NumIn()
   228  	if ft.IsVariadic() {
   229  		inReg--
   230  		if len(c.in) < inReg {
   231  			c.errorf(t, ": input has %d arguments; func requires at least %d", len(c.in), inReg)
   232  			return
   233  		}
   234  	} else if len(c.in) != ft.NumIn() {
   235  		c.errorf(t, ": input has %d arguments; func takes %d", len(c.in), ft.NumIn())
   236  		return
   237  	}
   238  
   239  	inv := make([]reflect.Value, len(c.in))
   240  	for i, v := range c.in {
   241  		if v == nil {
   242  			inv[i] = nilEmptyInterface
   243  		} else {
   244  			inv[i] = reflect.ValueOf(v)
   245  		}
   246  	}
   247  	got, didPanic, panicValue := f.checkCall(inv)
   248  
   249  	var goti []interface{}
   250  	if !didPanic {
   251  		goti = make([]interface{}, len(got))
   252  		for i, rv := range got {
   253  			goti[i] = rv.Interface()
   254  		}
   255  	}
   256  
   257  	if c.want != nil {
   258  		if !reflect.DeepEqual(goti, c.want) {
   259  			c.errorf(t, " = %v; want %v", formatRes(goti), formatRes(c.want))
   260  		}
   261  	}
   262  	for _, checkRes := range c.checkRes {
   263  		err := checkRes(Result{
   264  			Result:   goti,
   265  			Panic:    panicValue,
   266  			Panicked: didPanic,
   267  		})
   268  		if err != nil {
   269  			c.errorf(t, ": %v", err)
   270  		}
   271  	}
   272  	if didPanic && (c.checkRes == nil) {
   273  		c.errorf(t, ": panicked with %v", panicValue)
   274  	}
   275  }
   276  
   277  func formatRes(res []interface{}) string {
   278  	var buf bytes.Buffer
   279  	if len(res) != 1 {
   280  		buf.WriteByte('(')
   281  	}
   282  	formatValues(&buf, res)
   283  	if len(res) != 1 {
   284  		buf.WriteByte(')')
   285  	}
   286  	return buf.String()
   287  }
   288  
   289  func formatValues(buf *bytes.Buffer, vals []interface{}) {
   290  	for i, v := range vals {
   291  		if i != 0 {
   292  			buf.WriteString(", ")
   293  		}
   294  		fmt.Fprintf(buf, "%#v", v)
   295  	}
   296  }
   297  
   298  func (c *Case) errorf(t testing.TB, format string, args ...interface{}) {
   299  	var buf bytes.Buffer
   300  	if c.name != "" {
   301  		fmt.Fprintf(&buf, "%s: ", c.name)
   302  	}
   303  	buf.WriteString(c.f.Name)
   304  	buf.WriteString("(")
   305  	formatValues(&buf, c.in)
   306  	buf.WriteString(")")
   307  	fmt.Fprintf(&buf, format, args...)
   308  	t.Errorf("%s", buf.Bytes())
   309  }