-
Notifications
You must be signed in to change notification settings - Fork 10
/
let.go
175 lines (159 loc) · 5.5 KB
/
let.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package testcase
import (
"fmt"
"go.llib.dev/testcase/internal/caller"
"go.llib.dev/testcase/internal/reflects"
)
// Let define a memoized helper method.
// Let creates lazily-evaluated test execution bound variables.
// Let variables don't exist until called into existence by the actual tests,
// so you won't waste time loading them for examples that don't use them.
// They're also memoized, so they're useful for encapsulating database objects, due to the cost of making a database request.
// The value will be cached across list use within the same test execution but not across different test cases.
// You can eager load a value defined in let by referencing to it in a Before hook.
// Let is threadsafe, the parallel running test will receive they own test variable instance.
//
// Defining a value in a spec Context will ensure that the scope
// and it's nested scopes of the current scope will have access to the value.
// It cannot leak its value outside from the current scope.
// Calling Let in a nested/sub scope will apply the new value for that value to that scope and below.
//
// It will panic if it is used after a When/And/Then scope definition,
// because those scopes would have no clue about the later defined variable.
// In order to keep the specification reading mental model requirement low,
// it is intentionally not implemented to handle such case.
// Defining test vars always expected in the beginning of a specification scope,
// mainly for readability reasons.
//
// vars strictly belong to a given `Describe`/`When`/`And` scope,
// and configured before any hook would be applied,
// therefore hooks always receive the most latest version from the `Let` vars,
// regardless in which scope the hook that use the variable is define.
//
// Let can enhance readability
// when used sparingly in any given example group,
// but that can quickly degrade with heavy overuse.
func Let[V any](spec *Spec, blk VarInit[V]) Var[V] {
helper(spec.testingTB).Helper()
return let[V](spec, makeVarID(spec), blk)
}
type tuple2[V, B any] struct {
V V
B B
}
// Let2 is a tuple-style variable creation method, where an init block is shared between different variables.
func Let2[V, B any](spec *Spec, blk func(*T) (V, B)) (Var[V], Var[B]) {
helper(spec.testingTB).Helper()
src := Let[tuple2[V, B]](spec, func(t *T) tuple2[V, B] {
v, b := blk(t)
return tuple2[V, B]{V: v, B: b}
})
return Let[V](spec, func(t *T) V {
return src.Get(t).V
}), Let[B](spec, func(t *T) B {
return src.Get(t).B
})
}
type tuple3[V, B, N any] struct {
V V
B B
N N
}
// Let3 is a tuple-style variable creation method, where an init block is shared between different variables.
func Let3[V, B, N any](spec *Spec, blk func(*T) (V, B, N)) (Var[V], Var[B], Var[N]) {
helper(spec.testingTB).Helper()
src := Let[tuple3[V, B, N]](spec, func(t *T) tuple3[V, B, N] {
v, b, n := blk(t)
return tuple3[V, B, N]{V: v, B: b, N: n}
})
return Let[V](spec, func(t *T) V {
return src.Get(t).V
}), Let[B](spec, func(t *T) B {
return src.Get(t).B
}), Let[N](spec, func(t *T) N {
return src.Get(t).N
})
}
const panicMessageForLetValue = `%T literal can't be used with #LetValue
as the current implementation can't guarantee that the mutations on the value will not leak orderingOutput to other tests,
please use the #Let memorization helper for now`
// LetValue is a shorthand for defining immutable vars with Let under the hood.
// So the function blocks can be skipped, which makes tests more readable.
func LetValue[V any](spec *Spec, value V) Var[V] {
helper(spec.testingTB).Helper()
return letValue[V](spec, makeVarID(spec), value)
}
func let[V any](spec *Spec, varID VarID, blk VarInit[V]) Var[V] {
helper(spec.testingTB).Helper()
if spec.immutable {
spec.testingTB.Fatalf(warnEventOnImmutableFormat, `Let`)
}
if blk != nil {
spec.vars.defsSuper[varID] = findCurrentDeclsFor(spec, varID)
spec.vars.defs[varID] = func(t *T) any {
t.Helper()
return blk(t)
}
}
return Var[V]{ID: varID, Init: blk}
}
func letValue[V any](spec *Spec, varName VarID, value V) Var[V] {
helper(spec.testingTB).Helper()
if reflects.IsMutable(value) {
spec.testingTB.Fatalf(panicMessageForLetValue, value)
}
return let[V](spec, varName, func(t *T) V {
t.Helper()
v := value // pass by value copy
return v
})
}
// latest decl is the first and the deeper you want to reach back, the higher the index
func findCurrentDeclsFor(spec *Spec, varName VarID) []variablesInitBlock {
var decls []variablesInitBlock
for _, s := range spec.specsFromCurrent() {
if decl, ok := s.vars.defs[varName]; ok {
decls = append(decls, decl)
}
}
return decls
}
func makeVarID(spec *Spec) VarID {
helper(spec.testingTB).Helper()
location := caller.GetLocation(false)
// when variable is declared within a loop
// providing a variable ID offset is required to identify the variable uniquely.
varIDIndex := make(map[VarID]struct{})
for _, s := range spec.specsFromParent() {
for k := range s.vars.locks {
varIDIndex[k] = struct{}{}
}
for k := range s.vars.defs {
varIDIndex[k] = struct{}{}
}
for k := range s.vars.onLet {
varIDIndex[k] = struct{}{}
}
for k := range s.vars.before {
varIDIndex[k] = struct{}{}
}
}
var (
id VarID
offset int
)
positioning:
for {
// quick path for the majority of the case.
if _, ok := varIDIndex[VarID(location)]; !ok {
id = VarID(location)
break positioning
}
offset++
id = VarID(fmt.Sprintf("%s#[%d]", location, offset))
if _, ok := varIDIndex[id]; !ok {
break positioning
}
}
return id
}