Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

faster Run for known types #166

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmarks could be moved to a separate PR as they would be useful for the other alternatives.

10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ require (
github.com/kr/pretty v0.3.1
)

go 1.13
require (
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
)

// We do actually support go 1.14, but until go 1.21
// we can't have any generics code even if guarded
// by a build tag.
go 1.18
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]:
return fastRun1(c, name, f, t), true
case runner[*testing.B]:
return fastRun1(c, name, f, t), true
case runner[*C]:
return fastRun1(c, name, f, t), true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return fastRun(t, name, f) here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be possible as a shortcut, but maybe a bit too clever. In fact this shortcut assumes that t is if of type *qt.C which may not be the case (a non-qt.C might have a Run(string, *qt.C) bool method).

Another alternative which is less clever, but still shortcuts (avoids to create one qt.C) while not corrupting the interface:

Suggested change
return fastRun1(c, name, f, t), true
return t.Run(name, f), true

Note that this shortcut is already implemented in #165.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I thought about this, and even implemented it, but decided it wasn't a good idea. For one, it only saves one allocation, which is a fairly minor saving in this context. For another, it means that it's not consistent with the other types, which always allocate a new C. Making this optimisation changes observable behaviour and because I'm not convinced it's a worthwhile optimisation anyway, I'd be inclined to leave it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

One observable difference would be the format function that should be propagated from the parent C. We obviously lack a test that would have caught this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test proposed in #167.

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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very nice way of not exposing generics in the public API!

Run(name string, f func(T)) bool
}

func fastRun1[T testing.TB](c *C, name string, f func(*C), t runner[T]) bool {
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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just if called != 1?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also maybe s/called/callCount/, as called feels like a boolean.

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
}
Loading