Skip to content

Commit

Permalink
faster Run for known types
Browse files Browse the repository at this point in the history
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%
```
  • Loading branch information
rogpeppe committed Aug 1, 2023
1 parent 950cb4b commit fecf9c7
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 0 deletions.
29 changes: 29 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package quicktest_test

import (
"testing"

qt "github.com/frankban/quicktest"
)

func BenchmarkCNewAndRunWithCustomType(b *testing.B) {
for i := 0; i < b.N; i++ {
c := qt.New(customTForBenchmark{})
c.Run("test", func(c *qt.C) {})
}
}

func BenchmarkCRunWithCustomType(b *testing.B) {
c := qt.New(customTForBenchmark{})
for i := 0; i < b.N; i++ {
c.Run("test", func(c *qt.C) {})
}
}

type customTForBenchmark struct {
testing.TB
}

func (customTForBenchmark) Run(name string, f func(testing.TB)) bool {
return true
}
4 changes: 4 additions & 0 deletions quicktest.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ 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 {
r, ok := fastRun(c, name, f)
if ok {
return r
}
badType := func(m string) {
panic(fmt.Sprintf("cannot execute Run with underlying concrete type %T (%s)", c.TB, m))
}
Expand Down
41 changes: 41 additions & 0 deletions run_go1.18.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed under the MIT license, see LICENSE file for details.

//go:build go1.18
// +build go1.18

package quicktest

import "testing"

// fastRun implements c.Run for some known types.
// It returns the result of calling c.Run and also reports
// whether it was able to do so.
func fastRun(c *C, name string, f func(c *C)) (bool, bool) {
switch t := c.TB.(type) {
case runner[*testing.T]:

Check failure on line 15 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 15 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 15 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
return fastRun1(c, name, f, t), true

Check failure on line 16 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 16 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 16 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
case runner[*testing.B]:

Check failure on line 17 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 17 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 17 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
return fastRun1(c, name, f, t), true

Check failure on line 18 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 18 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 18 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
case runner[*C]:

Check failure on line 19 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 19 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 19 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
return fastRun1(c, name, f, t), true

Check failure on line 20 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 20 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 20 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

implicit function instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
case runner[testing.TB]:
// This case is here mostly for benchmarking, because
// it's hard to create a working concrete instance of *testing.T
// that isn't part of the outer tests.
return fastRun1(c, name, f, t), true
}
return false, false
}

type runner[T any] interface {

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

predeclared any requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

predeclared any requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 30 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

undeclared name: any (requires version go1.18 or later)
Run(name string, f func(T)) bool
}

func fastRun1[T testing.TB](c *C, name string, f func(*C), t runner[T]) bool {

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.20)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.19)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type parameter requires go1.18 or later (-lang was set to go1.13; check go.mod)

Check failure on line 34 in run_go1.18.go

View workflow job for this annotation

GitHub Actions / Build and Test (1.18)

type instantiation requires go1.18 or later (-lang was set to go1.13; check go.mod)
return t.Run(name, func(t2 T) {
c2 := New(t2)
defer c2.Done()
c2.SetFormat(c.getFormat())
f(c2)
})
}
60 changes: 60 additions & 0 deletions run_go1.18_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed under the MIT license, see LICENSE file for details.

//go:build go1.18
// +build go1.18

package quicktest_test

import (
"reflect"
"testing"

qt "github.com/frankban/quicktest"
)

type customT2[T testing.TB] struct {
testing.TB
}

func (t *customT2[T]) Run(name string, f func(T)) bool {
f(*new(T))
return true
}

func (t *customT2[T]) rtype() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
}

type otherTB struct {
testing.TB
}

func TestCRunCustomTypeWithNonMatchingRunSignature(t *testing.T) {
// Note: this test runs only on >=go1.18 because there isn't any
// code that specializes on this types that's enabled on versions before that.
tests := []interface {
testing.TB
rtype() reflect.Type
}{
&customT2[*testing.T]{},
&customT2[*testing.B]{},
&customT2[*qt.C]{},
&customT2[testing.TB]{},
&customT2[otherTB]{},
}
for _, test := range tests {
t.Run(test.rtype().String(), func(t *testing.T) {
c := qt.New(test)
called := 0
c.Run("test", func(c *qt.C) {
called++
if test.rtype().Kind() != reflect.Interface && reflect.TypeOf(c.TB) != test.rtype() {
t.Errorf("TB isn't expected type (want %v got %T)", test.rtype(), c.TB)
}
})
if got, want := called, 1; got != want {
t.Errorf("subtest was called %d times, not once", called)
}
})
}
}
10 changes: 10 additions & 0 deletions run_legacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed under the MIT license, see LICENSE file for details.

//go:build !go1.18
// +build !go1.18

package quicktest

func fastRun(c *C, name string, f func(c *C)) (bool, bool) {
return false, false
}

0 comments on commit fecf9c7

Please sign in to comment.