Skip to content

Commit 0fd803a

Browse files
committed
test: Experimental fixture package
Small helpers for building test fixtures
1 parent 81f8ce8 commit 0fd803a

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package fixture
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"testing"
7+
)
8+
9+
type FixtureInit interface {
10+
// Exported name for a helper, as it's intended to eventually make it to its
11+
// own package.
12+
13+
SetTB(testing.TB)
14+
}
15+
16+
type Setuper interface{ Setup() }
17+
18+
// NullSetuper is just a Setuper that ignores setup calls on the null instance
19+
type NullSetup struct{}
20+
21+
type setups []Setuper
22+
23+
func (s setups) Setup() {
24+
for _, ss := range s {
25+
ss.Setup()
26+
}
27+
}
28+
29+
func (s *setups) append(setup Setuper) {
30+
*s = append(*s, setup)
31+
}
32+
33+
func (s *NullSetup) Setup() {}
34+
35+
type Fixture struct {
36+
// Exported name for a helper, as it's intended to eventually make it to its
37+
// own package.
38+
39+
testing.TB
40+
}
41+
42+
func (f *Fixture) SetTB(tb testing.TB) { f.TB = tb }
43+
44+
func typePkgPath(t reflect.Type) (string, bool) {
45+
k := t.Kind()
46+
if k == reflect.Pointer {
47+
t = t.Elem()
48+
k = t.Kind()
49+
}
50+
if k == reflect.Struct {
51+
return t.PkgPath(), true
52+
}
53+
return "", false
54+
}
55+
56+
// valuePkgPath is specifically for filtering struct types based on their
57+
// package path. The use of an ok return value is solely for this specific
58+
// purpose alone; and the function may not be a good fit in other applications.
59+
func valuePkgPath(val reflect.Value) (string, bool) {
60+
t := val.Type()
61+
return typePkgPath(t)
62+
}
63+
64+
func (f *FixtureSetup[T]) Setup() Setuper {
65+
vType := reflect.TypeFor[T]()
66+
f.TB.Helper()
67+
if vType.Kind() != reflect.Pointer {
68+
f.TB.Fatalf("InitFixture: Fixture must be a pointer. Actual type: %s", vType.Name())
69+
}
70+
vType = vType.Elem()
71+
f.pkgPath = vType.PkgPath()
72+
return f.setup(reflect.ValueOf(f.Fixture))
73+
}
74+
75+
func (f FixtureSetup[T]) defaultInclude(val reflect.Value) bool {
76+
var currPath, expPath string
77+
var ok bool
78+
if currPath, ok = valuePkgPath(val); ok {
79+
expPath, ok = typePkgPath(reflect.TypeFor[T]())
80+
}
81+
if !ok {
82+
return false
83+
}
84+
return strings.HasPrefix(currPath, expPath)
85+
}
86+
87+
func (f FixtureSetup[T]) include(val reflect.Value) bool {
88+
if f.Include != nil {
89+
return f.Include(val)
90+
}
91+
return f.defaultInclude(val)
92+
}
93+
94+
type FixtureSetup[T FixtureInit] struct {
95+
TB testing.TB
96+
Fixture T
97+
Include func(val reflect.Value) bool
98+
Deps []any
99+
100+
pkgPath string
101+
}
102+
103+
func (f FixtureSetup[T]) setup(val reflect.Value) Setuper {
104+
var setups = new(setups)
105+
if !f.include(val) {
106+
return setups
107+
}
108+
if val.Kind() == reflect.Pointer {
109+
if val.IsNil() {
110+
return &NullSetup{}
111+
}
112+
val = val.Elem()
113+
}
114+
typ := val.Type()
115+
for _, field := range reflect.VisibleFields(typ) {
116+
if len(field.Index) > 1 { // Don't set fields of embedded members
117+
continue
118+
}
119+
for _, dep := range f.Deps {
120+
depVal := reflect.ValueOf(dep)
121+
if field.Type == reflect.TypeOf(dep) {
122+
val.FieldByIndex(field.Index).Set(depVal)
123+
continue
124+
}
125+
}
126+
setups.append(f.setup(val.FieldByIndex(field.Index)))
127+
}
128+
if setup, ok := val.Interface().(Setuper); ok {
129+
setups.append(setup)
130+
} else if val.CanAddr() {
131+
if setup, ok := val.Addr().Interface().(Setuper); ok {
132+
setups.append(setup)
133+
}
134+
}
135+
// var res *NullSetup
136+
return setups
137+
}
138+
139+
func InitFixture[T FixtureInit](
140+
t testing.TB,
141+
fixture T,
142+
deps ...any,
143+
) (T, Setuper) {
144+
f := &FixtureSetup[T]{
145+
TB: t,
146+
Fixture: fixture,
147+
Deps: deps,
148+
}
149+
return fixture, f.Setup()
150+
}

internal/testing/exp/package.go

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package exp is a container for experimental packages with potential general purpose use.
2+
package exp

0 commit comments

Comments
 (0)