Skip to content

Commit e19b891

Browse files
committed
feat: add support for benchmarks (#144)
Signed-off-by: Tronje Krop <tronje.krop@jactors.de>
1 parent b827750 commit e19b891

4 files changed

Lines changed: 133 additions & 9 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,11 @@ project has more than 25 Stars, I will introduce semantic versions `v1`.
281281
If you like to contribute, please create an issue and/or pull request with a
282282
proper description of your proposal or contribution. I will review it and
283283
provide feedback on it as fast as possible.
284+
285+
286+
## Disclaimer
287+
288+
This software is `100%` written by humans. AI is only used to review code,
289+
search for bugs, improve test coverage, and analyse performance issues. All
290+
actions executed with the help of AI are carefully reviewed, checked, and
291+
modified with the human standards and quality goals in mind.

reflect/reflect.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,40 @@ func Name[P any](name string, param P) string {
306306
}
307307
return ""
308308
}
309+
310+
// Run executes a test with given name and test callback function on the given
311+
// target. The target is typically a test runner instance, such as `*testing.T`
312+
// or `*testing.B` and must implement `Run(name string, call func(T))` that is
313+
// called to execute the test.
314+
//
315+
// A panic is raised if the target has no `Run` method or if target type does
316+
// not implement T.
317+
func Run[T any](target any, name string, call func(T)) {
318+
value := reflect.ValueOf(target)
319+
if !value.IsValid() {
320+
panic("target must not be nil")
321+
}
322+
323+
method := value.MethodByName("Run")
324+
if !method.IsValid() {
325+
panic("target does not implement method [" +
326+
reflect.TypeOf(target).String() + " => Run]")
327+
}
328+
329+
// Build a typed func(*concreteT) that wraps the inner argument as T
330+
// before forwarding to the provided call function.
331+
wrapper := reflect.MakeFunc(method.Type().In(1),
332+
func(args []reflect.Value) []reflect.Value {
333+
iface := args[0].Interface()
334+
if typ, ok := iface.(T); ok {
335+
call(typ)
336+
return nil
337+
}
338+
339+
panic("type does not implement [" +
340+
reflect.TypeOf(iface).String() + " => " +
341+
reflect.TypeOf((*T)(nil)).Elem().String() + "]")
342+
})
343+
344+
method.Call([]reflect.Value{reflect.ValueOf(name), wrapper})
345+
}

reflect/reflect_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,3 +1154,83 @@ func TestName(t *testing.T) {
11541154
assert.Equal(t, param.expect, result)
11551155
})
11561156
}
1157+
1158+
// runner is a test double whose Run method delivers itself to the callback.
1159+
type runner struct {
1160+
called bool
1161+
}
1162+
1163+
func (r *runner) Run(_ string, call func(*runner)) bool {
1164+
call(r)
1165+
1166+
return true
1167+
}
1168+
1169+
// wrongRunner has a Run method that delivers a wrongRunner value. Since
1170+
// wrongRunner does not implement *runner, the type assertion in Run fails.
1171+
type wrongRunner struct{}
1172+
1173+
func (wrongRunner) Run(_ string, f func(wrongRunner)) bool {
1174+
f(wrongRunner{})
1175+
1176+
return true
1177+
}
1178+
1179+
// RunParams holds the parameters for TestRun.
1180+
type RunParams struct {
1181+
target any
1182+
name string
1183+
expect mock.SetupFunc
1184+
check func(test.Test, *runner)
1185+
}
1186+
1187+
var runTestCases = map[string]RunParams{
1188+
// Happy path.
1189+
"run": {
1190+
target: &runner{},
1191+
name: "sub",
1192+
check: func(t test.Test, r *runner) {
1193+
assert.True(t, r.called)
1194+
},
1195+
},
1196+
1197+
// Error paths.
1198+
"nil-target": {
1199+
name: "sub",
1200+
expect: test.Panic("target must not be nil"),
1201+
},
1202+
1203+
"no-run-method": {
1204+
target: struct{}{},
1205+
name: "sub",
1206+
expect: test.Panic("target does not implement method " +
1207+
"[struct {} => Run]"),
1208+
},
1209+
1210+
"inner-no-implement": {
1211+
target: wrongRunner{},
1212+
name: "sub",
1213+
expect: test.Panic(
1214+
"type does not implement " +
1215+
"[reflect_test.wrongRunner => *reflect_test.runner]"),
1216+
},
1217+
}
1218+
1219+
func TestRun(t *testing.T) {
1220+
test.Map(t, runTestCases).
1221+
Run(func(t test.Test, param RunParams) {
1222+
// Given
1223+
mock.NewMocks(t).Expect(param.expect)
1224+
r, _ := param.target.(*runner)
1225+
1226+
// When
1227+
reflect.Run[*runner](param.target, param.name, func(inner *runner) {
1228+
inner.called = true
1229+
})
1230+
1231+
// Then
1232+
if param.check != nil {
1233+
param.check(t, r)
1234+
}
1235+
})
1236+
}

test/runner.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"runtime"
88
"strconv"
99
"strings"
10-
"testing"
1110
"time"
1211

1312
"github.com/tkrop/go-testing/internal/maps"
@@ -130,7 +129,7 @@ type Factory[P any] interface {
130129
// factory is a generic parameterized test factory struct.
131130
type factory[P any] struct {
132131
// The testing context to run the tests in.
133-
t *testing.T
132+
t Test
134133
// A wait group to synchronize the test execution.
135134
wg sync.WaitGroup
136135
// The test parameter sets to run.
@@ -148,7 +147,7 @@ type factory[P any] struct {
148147
// can be a single test parameter set, a slice of test parameter sets, or a map
149148
// of named test parameter sets. The test runner is looking into the parameter
150149
// set to determine a suitable test case name, e.g. by using a `name` parameter.
151-
func Any[P any](t *testing.T, params any) Factory[P] {
150+
func Any[P any](t Test, params any) Factory[P] {
152151
t.Helper()
153152

154153
return &factory[P]{
@@ -161,7 +160,7 @@ func Any[P any](t *testing.T, params any) Factory[P] {
161160
// Param creates a new parallel test runner with given test parameter sets
162161
// provided as variadic arguments. The test runner is looking into the
163162
// parameter set to find a suitable test case name.
164-
func Param[P any](t *testing.T, params ...P) Factory[P] {
163+
func Param[P any](t Test, params ...P) Factory[P] {
165164
t.Helper()
166165

167166
if len(params) == 1 {
@@ -172,7 +171,7 @@ func Param[P any](t *testing.T, params ...P) Factory[P] {
172171

173172
// Map creates a new parallel test runner with given test parameter sets
174173
// provided as a test case name to parameter sets mapping.
175-
func Map[P any](t *testing.T, params ...map[string]P) Factory[P] {
174+
func Map[P any](t Test, params ...map[string]P) Factory[P] {
176175
t.Helper()
177176

178177
return Any[P](t, maps.Add(maps.Copy(params[0]), params[1:]...))
@@ -181,7 +180,7 @@ func Map[P any](t *testing.T, params ...map[string]P) Factory[P] {
181180
// Slice creates a new parallel test runner with given test parameter sets
182181
// provided as a slice. The test runner is looking into the parameter set to
183182
// find a suitable test case name.
184-
func Slice[P any](t *testing.T, params ...[]P) Factory[P] {
183+
func Slice[P any](t Test, params ...[]P) Factory[P] {
185184
t.Helper()
186185

187186
return Any[P](t, slices.Add(params...))
@@ -297,16 +296,16 @@ func (r *factory[P]) exec(
297296
return
298297
}
299298

300-
r.t.Run(name, r.wrap(param, call, parallel))
299+
reflect.Run(r.t, name, r.wrap(param, call, parallel))
301300
}
302301

303302
// wrap creates the wrapper method eventually executing the test.
304303
func (r *factory[P]) wrap(
305304
param P, call ParamFunc[P], parallel bool,
306-
) func(*testing.T) {
305+
) func(Test) {
307306
r.wg.Add(1)
308307

309-
return func(t *testing.T) {
308+
return func(t Test) {
310309
t.Helper()
311310

312311
New(t, parallel).

0 commit comments

Comments
 (0)