Skip to content

Commit fecf9c7

Browse files
committed
faster Run for known types
This is an alternative to PRs #160 and #165. It's essentially the same as PR #165 except that it uses generics to reduce the amount of duplicated code. Instead of just amortizing the checking of the type, when the argument type of the function passed to `Run` is known, it bypasses the reflect-based code altogether. We don't bother implementing the optimization on pre-generics Go versions because those are end-of-lifetime anyway. I've added an implementation-independent benchmark. ``` goos: linux goarch: amd64 pkg: github.com/frankban/quicktest cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz │ base │ thisPR │ │ sec/op │ sec/op vs base │ CNewAndRunWithCustomType-8 1077.5n ± 5% 136.8n ± 6% -87.30% (p=0.002 n=6) CRunWithCustomType-8 1035.00n ± 11% 66.43n ± 3% -93.58% (p=0.002 n=6) geomean 1.056µ 95.33n -90.97% ```
1 parent 950cb4b commit fecf9c7

5 files changed

Lines changed: 144 additions & 0 deletions

File tree

bench_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package quicktest_test
2+
3+
import (
4+
"testing"
5+
6+
qt "github.com/frankban/quicktest"
7+
)
8+
9+
func BenchmarkCNewAndRunWithCustomType(b *testing.B) {
10+
for i := 0; i < b.N; i++ {
11+
c := qt.New(customTForBenchmark{})
12+
c.Run("test", func(c *qt.C) {})
13+
}
14+
}
15+
16+
func BenchmarkCRunWithCustomType(b *testing.B) {
17+
c := qt.New(customTForBenchmark{})
18+
for i := 0; i < b.N; i++ {
19+
c.Run("test", func(c *qt.C) {})
20+
}
21+
}
22+
23+
type customTForBenchmark struct {
24+
testing.TB
25+
}
26+
27+
func (customTForBenchmark) Run(name string, f func(testing.TB)) bool {
28+
return true
29+
}

quicktest.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ var (
243243
// A panic is raised when Run is called and the embedded concrete type does not
244244
// implement a Run method with a correct signature.
245245
func (c *C) Run(name string, f func(c *C)) bool {
246+
r, ok := fastRun(c, name, f)
247+
if ok {
248+
return r
249+
}
246250
badType := func(m string) {
247251
panic(fmt.Sprintf("cannot execute Run with underlying concrete type %T (%s)", c.TB, m))
248252
}

run_go1.18.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed under the MIT license, see LICENSE file for details.
2+
3+
//go:build go1.18
4+
// +build go1.18
5+
6+
package quicktest
7+
8+
import "testing"
9+
10+
// fastRun implements c.Run for some known types.
11+
// It returns the result of calling c.Run and also reports
12+
// whether it was able to do so.
13+
func fastRun(c *C, name string, f func(c *C)) (bool, bool) {
14+
switch t := c.TB.(type) {
15+
case runner[*testing.T]:
16+
return fastRun1(c, name, f, t), true
17+
case runner[*testing.B]:
18+
return fastRun1(c, name, f, t), true
19+
case runner[*C]:
20+
return fastRun1(c, name, f, t), true
21+
case runner[testing.TB]:
22+
// This case is here mostly for benchmarking, because
23+
// it's hard to create a working concrete instance of *testing.T
24+
// that isn't part of the outer tests.
25+
return fastRun1(c, name, f, t), true
26+
}
27+
return false, false
28+
}
29+
30+
type runner[T any] interface {
31+
Run(name string, f func(T)) bool
32+
}
33+
34+
func fastRun1[T testing.TB](c *C, name string, f func(*C), t runner[T]) bool {
35+
return t.Run(name, func(t2 T) {
36+
c2 := New(t2)
37+
defer c2.Done()
38+
c2.SetFormat(c.getFormat())
39+
f(c2)
40+
})
41+
}

run_go1.18_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Licensed under the MIT license, see LICENSE file for details.
2+
3+
//go:build go1.18
4+
// +build go1.18
5+
6+
package quicktest_test
7+
8+
import (
9+
"reflect"
10+
"testing"
11+
12+
qt "github.com/frankban/quicktest"
13+
)
14+
15+
type customT2[T testing.TB] struct {
16+
testing.TB
17+
}
18+
19+
func (t *customT2[T]) Run(name string, f func(T)) bool {
20+
f(*new(T))
21+
return true
22+
}
23+
24+
func (t *customT2[T]) rtype() reflect.Type {
25+
return reflect.TypeOf((*T)(nil)).Elem()
26+
}
27+
28+
type otherTB struct {
29+
testing.TB
30+
}
31+
32+
func TestCRunCustomTypeWithNonMatchingRunSignature(t *testing.T) {
33+
// Note: this test runs only on >=go1.18 because there isn't any
34+
// code that specializes on this types that's enabled on versions before that.
35+
tests := []interface {
36+
testing.TB
37+
rtype() reflect.Type
38+
}{
39+
&customT2[*testing.T]{},
40+
&customT2[*testing.B]{},
41+
&customT2[*qt.C]{},
42+
&customT2[testing.TB]{},
43+
&customT2[otherTB]{},
44+
}
45+
for _, test := range tests {
46+
t.Run(test.rtype().String(), func(t *testing.T) {
47+
c := qt.New(test)
48+
called := 0
49+
c.Run("test", func(c *qt.C) {
50+
called++
51+
if test.rtype().Kind() != reflect.Interface && reflect.TypeOf(c.TB) != test.rtype() {
52+
t.Errorf("TB isn't expected type (want %v got %T)", test.rtype(), c.TB)
53+
}
54+
})
55+
if got, want := called, 1; got != want {
56+
t.Errorf("subtest was called %d times, not once", called)
57+
}
58+
})
59+
}
60+
}

run_legacy.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed under the MIT license, see LICENSE file for details.
2+
3+
//go:build !go1.18
4+
// +build !go1.18
5+
6+
package quicktest
7+
8+
func fastRun(c *C, name string, f func(c *C)) (bool, bool) {
9+
return false, false
10+
}

0 commit comments

Comments
 (0)