diff --git a/export_test.go b/export_test.go index 06b17b7..9929ffb 100644 --- a/export_test.go +++ b/export_test.go @@ -5,4 +5,7 @@ package quicktest var ( Prefixf = prefixf TestingVerbose = &testingVerbose + + GetRunFuncSignature = getRunFuncSignature + GetRunFuncSignatureCache = getRunFuncSignatureCache ) diff --git a/quicktest.go b/quicktest.go index 65243cf..3824af3 100644 --- a/quicktest.go +++ b/quicktest.go @@ -217,6 +217,60 @@ var ( tbType = reflect.TypeOf(new(testing.TB)).Elem() ) +// getRunFuncSignature checks the signature of the Run method of the type (ex: *testing.T) +// and returns the signature of its function argument (func(t *T) for *testing.T). +func getRunFuncSignature(t reflect.Type) (reflect.Type, error) { + badType := func(detail string) (reflect.Type, error) { + return nil, fmt.Errorf("cannot execute Run with underlying concrete type %s (%s)", t, detail) + } + m, ok := t.MethodByName("Run") + if !ok { + // c.TB doesn't implement a Run method. + return badType("no Run method") + } + mt := m.Type + // fmt.Println(mt) + if mt.NumIn() != 3 || + mt.In(1) != stringType || + mt.NumOut() != 1 || + mt.Out(0) != boolType { + // The Run method doesn't have the right argument counts and types. + return badType("wrong argument count for Run method") + } + farg := mt.In(2) + if farg.Kind() != reflect.Func || + farg.NumIn() != 1 || + farg.NumOut() != 0 || + !farg.In(0).AssignableTo(tbType) { + // The first argument to the Run function arg isn't right. + return badType("bad first argument type for Run method") + } + + return farg, nil +} + +// Cache of getRunFuncSignature results used by getRunFuncSignatureCache. +var runFuncSigCache = []struct { + T reflect.Type + FArg reflect.Type +}{ + // Prefill with common types + // Use TestCRunGetFuncSig to generate values + {T: reflect.TypeOf((*testing.T)(nil)), FArg: reflect.TypeOf((func(*testing.T))(nil))}, + {T: reflect.TypeOf((*testing.B)(nil)), FArg: reflect.TypeOf((func(*testing.B))(nil))}, + {T: reflect.TypeOf((*C)(nil)), FArg: reflect.TypeOf((func(*C))(nil))}, +} + +func getRunFuncSignatureCache(t reflect.Type) (reflect.Type, error) { + for i := 0; i < len(runFuncSigCache); i++ { + if t == runFuncSigCache[i].T { + return runFuncSigCache[i].FArg, nil + } + } + + return getRunFuncSignature(t) +} + // Run runs f as a subtest of t called name. It's a wrapper around // the Run method of c.TB that provides the quicktest checker to f. When // the function completes, c.Done will be called to run any @@ -243,30 +297,12 @@ var ( // A panic is raised when Run is called and the embedded concrete type does not // implement a Run method with a correct signature. func (c *C) Run(name string, f func(c *C)) bool { - badType := func(m string) { - panic(fmt.Sprintf("cannot execute Run with underlying concrete type %T (%s)", c.TB, m)) - } - m := reflect.ValueOf(c.TB).MethodByName("Run") - if !m.IsValid() { - // c.TB doesn't implement a Run method. - badType("no Run method") - } - mt := m.Type() - if mt.NumIn() != 2 || - mt.In(0) != stringType || - mt.NumOut() != 1 || - mt.Out(0) != boolType { - // The Run method doesn't have the right argument counts and types. - badType("wrong argument count for Run method") - } - farg := mt.In(1) - if farg.Kind() != reflect.Func || - farg.NumIn() != 1 || - farg.NumOut() != 0 || - !farg.In(0).AssignableTo(tbType) { - // The first argument to the Run function arg isn't right. - badType("bad first argument type for Run method") + + farg, err := getRunFuncSignatureCache(reflect.TypeOf(c.TB)) + if err != nil { + panic(err.Error()) } + cFormat := c.getFormat() fv := reflect.MakeFunc(farg, func(args []reflect.Value) []reflect.Value { c2 := New(args[0].Interface().(testing.TB)) @@ -275,6 +311,8 @@ func (c *C) Run(name string, f func(c *C)) bool { f(c2) return nil }) + + m := reflect.ValueOf(c.TB).MethodByName("Run") return m.Call([]reflect.Value{reflect.ValueOf(name), fv})[0].Interface().(bool) } diff --git a/quicktest_test.go b/quicktest_test.go index ec23afb..7cd3e23 100644 --- a/quicktest_test.go +++ b/quicktest_test.go @@ -6,6 +6,7 @@ import ( "bytes" "errors" "fmt" + "reflect" "strings" "testing" @@ -527,6 +528,47 @@ want: `) } +// TestCRunGetFuncSig checks the internal function getRunFuncSignature which is used in Run. +func TestCRunGetFuncSig(t *testing.T) { + for _, tb := range []testing.TB{ + t, // *testing.T + (*testing.B)(nil), // *testing.B + qt.New(t), // *quicktest.C + } { + tbt := reflect.TypeOf(tb) + farg, err := qt.GetRunFuncSignature(tbt) + if err != nil { + t.Error(err) + continue + } + t.Logf("(%s).Run: func(string, %s) bool", tbt, farg) + // useful to create a cache + t.Logf("{T: reflect.TypeOf((%s)(nil)), FArg: reflect.TypeOf((%s)(nil))}", tbt, farg) + } +} + +func BenchmarkCRunGetFuncSig(b *testing.B) { + for _, tb := range []testing.TB{ + (*testing.T)(nil), // *testing.T + b, // *testing.B + qt.New(b), // *quicktest.C + } { + tbt := reflect.TypeOf(tb) + + b.Run(tbt.String()+"-no-cache", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = qt.GetRunFuncSignature(tbt) + } + }) + + b.Run(tbt.String()+"-with-cache", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = qt.GetRunFuncSignatureCache(tbt) + } + }) + } +} + func TestHelper(t *testing.T) { tt := &testingT{} qt.Assert(tt, true, qt.IsFalse)