-
Notifications
You must be signed in to change notification settings - Fork 10
/
let.go
125 lines (113 loc) · 4.21 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
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] {
spec.testingTB.Helper()
return let[V](spec, makeVarName(spec), blk)
}
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] {
spec.testingTB.Helper()
return letValue[V](spec, makeVarName(spec), value)
}
func let[V any](spec *Spec, varName string, blk VarInit[V]) Var[V] {
spec.testingTB.Helper()
if spec.immutable {
spec.testingTB.Fatalf(warnEventOnImmutableFormat, `Let`)
}
if blk != nil {
spec.vars.defsSuper[varName] = findCurrentDeclsFor(spec, varName)
spec.vars.defs[varName] = func(t *T) any {
t.Helper()
return blk(t)
}
}
return Var[V]{ID: varName, Init: blk}
}
func letValue[V any](spec *Spec, varName string, value V) Var[V] {
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 string) []variablesInitBlock {
var decls []variablesInitBlock
for _, s := range spec.specsFromCurrent() {
if decl, ok := s.vars.defs[varName]; ok {
decls = append(decls, decl)
}
}
return decls
}
func makeVarName(spec *Spec) string {
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.
varNameIndex := make(map[string]struct{})
for _, s := range spec.specsFromParent() {
for k := range s.vars.defs {
varNameIndex[k] = struct{}{}
}
}
var (
name string
offset int
)
positioning:
for {
// quick path for the majority of the case.
if _, ok := varNameIndex[location]; !ok {
name = location
break positioning
}
offset++
name = fmt.Sprintf("%s#[%d]", location, offset)
if _, ok := varNameIndex[name]; !ok {
break positioning
}
}
return name
}