-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate_test.go
More file actions
391 lines (351 loc) · 14.1 KB
/
Copy pathvalidate_test.go
File metadata and controls
391 lines (351 loc) · 14.1 KB
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package di_test
import (
"errors"
"strings"
"testing"
"golang.yandex/di"
)
// Fixtures for Validate: an application graph with a request-scoped service
// declared in the root and resolved through a child that provides the request.
type valCfg struct{}
type valDB struct{ cfg valCfg }
type valRepo struct{ db *valDB }
type valReq struct{}
type valUser struct{ req *valReq }
type valHandler struct {
repo *valRepo
user *valUser
}
type valMailer struct{ user *valUser }
type valA struct{ b *valB }
type valB struct{ a *valA }
func newValDB(cfg valCfg) *valDB { return &valDB{cfg} }
func newValRepo(db *valDB) *valRepo { return &valRepo{db} }
func newValUser(r *valReq) *valUser { return &valUser{r} }
func newValHandler(r *valRepo, u *valUser) *valHandler { return &valHandler{r, u} }
func newValMailer(u *valUser) *valMailer { return &valMailer{u} }
func newValA(b *valB) *valA { return &valA{b} }
func newValB(a *valA) *valB { return &valB{a} }
func appGraph(s *di.Scope) {
s.Value(valCfg{})
s.Wire[*valDB](newValDB)
s.Wire[*valRepo](newValRepo)
s.Wire[*valUser](newValUser).Scoped()
s.Wire[*valHandler](newValHandler).Scoped()
}
func has(lines []string, want string) bool {
for _, l := range lines {
if strings.Contains(l, want) {
return true
}
}
return false
}
func TestValidateCleanGraphOwesTheRequest(t *testing.T) {
app := di.New()
appGraph(app)
v := app.Validate()
if err := v.Err(); err != nil {
t.Fatalf("clean graph reported %v", err)
}
// *valUser is Scoped and needs *valReq, which only a request scope provides:
// owed, not an error, from the root.
if len(v.Owed) != 1 || !has(v.Owed, "valReq") || !has(v.Owed, "valUser") {
t.Fatalf("want *valReq owed by *valUser, got %v", v.Owed)
}
if len(v.Unchecked) != 0 {
t.Fatalf("nothing is a closure here, got %v", v.Unchecked)
}
}
func TestValidateFromTheRequestScopeChecksWhatItProvides(t *testing.T) {
app := di.New()
appGraph(app)
req := app.Child("request")
req.Value(&valReq{})
v := req.Validate()
if err := v.Err(); err != nil {
t.Fatal(err)
}
if len(v.Owed) != 0 {
t.Fatalf("the request scope provides *valReq, yet %v", v.Owed)
}
bare := app.Child("request")
if v := bare.Validate(); len(v.Owed) != 1 || v.Err() != nil {
t.Fatalf("a child without the request still owes it: %v / %v", v.Owed, v.Err())
}
}
func TestValidateMissingSingletonDependency(t *testing.T) {
s := di.New()
s.Wire[*valRepo](newValRepo) // *valDB is not provided
v := s.Validate()
if len(v.Errors) != 1 || !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("want one ErrNotProvided, got %v", v.Errors)
}
msg := v.Err().Error()
if !strings.Contains(msg, "valDB") || !strings.Contains(msg, "needed by [*golang.yandex/di_test.valRepo]") || !strings.Contains(msg, "validate_test.go") {
t.Fatalf("message should name the key, the path and the site: %s", msg)
}
}
func TestValidateSingletonCapturingAScopedIsAnErrorOnlyWhenItWouldFail(t *testing.T) {
// *valMailer is a singleton in the root that depends on *valUser, which is
// Scoped and needs the request. The root cannot provide it, so building
// *valMailer would fail there: that is proved, and an error.
app := di.New()
appGraph(app)
app.Wire[*valMailer](newValMailer)
v := app.Validate()
if len(v.Errors) != 1 || !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("want the capture reported once as ErrNotProvided, got %v", v.Errors)
}
if msg := v.Err().Error(); !strings.Contains(msg, "valMailer") || !strings.Contains(msg, "valUser") || !strings.Contains(msg, "Scoped") {
t.Fatalf("message should explain the capture: %s", msg)
}
// *valReq is still owed for the Scoped resolution path, independently.
if !has(v.Owed, "valReq") {
t.Fatalf("owed list lost the request: %v", v.Owed)
}
// The same shape where the Scoped service is satisfiable from the root
// is not an error: the singleton captures the root's own instance, which
// is legitimate and what the runtime does.
s := di.New()
s.Value(valCfg{})
s.Wire[*valDB](newValDB).Scoped()
s.Wire[*valRepo](newValRepo)
if v := s.Validate(); v.Err() != nil || len(v.Owed) != 0 {
t.Fatalf("a satisfiable Scoped dependency is not a failure: %v / %v", v.Err(), v.Owed)
}
}
func TestValidateFindsACycleAmongWireConstructors(t *testing.T) {
s := di.New()
s.Wire[*valA](newValA)
s.Wire[*valB](newValB)
v := s.Validate()
if !errors.Is(v.Err(), di.ErrCycle) {
t.Fatalf("want ErrCycle, got %v", v.Err())
}
// One cycle, one line, however many of its members take a turn.
if len(v.Errors) != 1 {
t.Fatalf("want the cycle reported once, got %v", v.Errors)
}
if msg := v.Errors[0].Error(); !strings.Contains(msg, "-> ") {
t.Fatalf("the message should draw the path: %s", msg)
}
}
func TestValidateCannotSeeThroughAClosure(t *testing.T) {
s := di.New()
s.Wire[*valA](newValA)
s.Provide(func(s *di.Scope) *valB { return &valB{s.Get[*valA]()} }) // the cycle's other half is opaque
v := s.Validate()
if v.Err() != nil {
t.Fatalf("a closure hides its dependencies, yet %v", v.Err())
}
if len(v.Unchecked) != 1 || !has(v.Unchecked, "valB") {
t.Fatalf("the closure should be listed as unchecked, got %v", v.Unchecked)
}
// The runtime still finds the cycle, which is why Unchecked exists.
if _, err := s.Resolve[*valA](); !errors.Is(err, di.ErrCycle) {
t.Fatalf("runtime should report the cycle, got %v", err)
}
}
func TestValidateChecksAncestorsFromAChild(t *testing.T) {
app := di.New()
app.Wire[*valRepo](newValRepo) // missing *valDB in the root
req := app.Child("request")
req.Value(&valDB{}) // provided in the child: does not help a root singleton
v := req.Validate()
if !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("a root singleton is built in the root, so the child's *valDB cannot satisfy it: %v", v.Err())
}
}
func TestValidateBuildsNothingAndIsRepeatable(t *testing.T) {
s := di.New()
built := false
s.Wire[*valDB](func(valCfg) *valDB { built = true; return &valDB{} })
s.Value(valCfg{})
s.Wire[*valRepo](newValRepo)
first := s.Validate()
second := s.Validate()
if built {
t.Fatal("Validate ran a constructor")
}
if first.Err() != nil || len(first.Owed) != len(second.Owed) || len(first.Unchecked) != len(second.Unchecked) {
t.Fatalf("results differ between calls: %+v / %+v", first, second)
}
}
func TestValidateReportsADiamondOnce(t *testing.T) {
s := di.New()
s.Wire[*valRepo](newValRepo)
s.Wire[*valUser](func(*valRepo) *valUser { return nil }).Scoped()
s.Wire[*valHandler](func(*valRepo, *valUser) *valHandler { return nil }).Scoped()
v := s.Validate()
// *valDB is missing for the singleton *valRepo: one error. The two Scoped
// bindings reach *valRepo, a singleton checked on its own turn, and add
// nothing.
if len(v.Errors) != 1 || len(v.Owed) != 0 {
t.Fatalf("want one error and no owed lines, got %v / %v", v.Errors, v.Owed)
}
}
func TestValidateGroupMembersAreChecked(t *testing.T) {
s := di.New()
s.Wire[*valRepo](newValRepo).Group()
s.Wire[*valRepo](func() *valRepo { return nil }).Group()
v := s.Validate()
if len(v.Errors) != 1 || !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("the member that needs *valDB should be reported once, got %v", v.Errors)
}
}
func TestValidateIgnoresAReplacedRegistration(t *testing.T) {
s := di.New()
s.Wire[*valRepo](newValRepo) // needs *valDB, which is missing
s.Wire[*valRepo](func() *valRepo { return &valRepo{} }).Override() // replaces it
if v := s.Validate(); v.Err() != nil {
t.Fatalf("the replaced registration no longer serves anything: %v", v.Err())
}
}
func TestValidateCommitsPendingLikeAResolution(t *testing.T) {
s := di.New()
s.Wire[*valRepo](newValRepo)
s.Wire[*valRepo](func() *valRepo { return nil }) // a collision without Override
defer func() {
msg, ok := recover().(string)
if !ok || !strings.HasPrefix(msg, "di: ") {
t.Fatalf("want the configuration rejection, got %v", msg)
}
}()
s.Validate()
t.Fatal("Validate accepted a colliding registration")
}
// The stubs describe the scope that will resolve a Scoped binding, so the
// check a request scope would make is made from the application scope: what
// the stubs cover is satisfied, and what remains unmet is an error rather
// than owed. Without stubs nothing changes.
func TestValidateWithStubsIsALeaf(t *testing.T) {
app := di.New()
appGraph(app)
v := app.Validate(di.Provided[*valReq]())
if err := v.Err(); err != nil || len(v.Owed) != 0 {
t.Fatalf("the stub covers the request, yet err=%v owed=%v", err, v.Owed)
}
app.Wire[*valMailer](func(*valUser, *valCfg) *valMailer { return nil }).Scoped() // *valCfg is registered as a value, but a *valCfg is not
v = app.Validate(di.Provided[*valReq]())
if !errors.Is(v.Err(), di.ErrNotProvided) || !strings.Contains(v.Err().Error(), "valCfg") || !strings.Contains(v.Err().Error(), "stubs") {
t.Fatalf("what neither the scope nor the stubs provide is an error at a leaf, got %v", v.Err())
}
if len(v.Owed) != 0 {
t.Fatalf("a leaf owes nothing, got %v", v.Owed)
}
if v := app.Validate(); v.Err() != nil || len(v.Owed) != 2 {
t.Fatalf("without stubs both are owed and neither is an error: %v / %v", v.Err(), v.Owed)
}
}
// A stub is a key, so an interface a request scope provides is stubbed by
// its interface type, and a stub satisfies only the Scoped path: a singleton
// that would build a Scoped service in its own scope still fails there.
func TestValidateStubsAreKeysAndDoNotReachSingletons(t *testing.T) {
type principal interface{ Name() string }
s := di.New()
s.Wire[*valUser](func(principal) *valUser { return nil }).Scoped()
if v := s.Validate(di.Provided[principal]()); v.Err() != nil {
t.Fatalf("an interface stub should satisfy the interface key: %v", v.Err())
}
app := di.New()
appGraph(app)
app.Wire[*valMailer](newValMailer) // a singleton capturing the request-scoped *valUser
v := app.Validate(di.Provided[*valReq]())
if len(v.Errors) != 1 || !strings.Contains(v.Err().Error(), "valMailer") {
t.Fatalf("the capture is a failure in root whatever a request scope holds, got %v", v.Errors)
}
}
func TestValidateWithStubsBuildsNothing(t *testing.T) {
built := false
app := di.New()
app.Wire[*valUser](func(*valReq) *valUser { built = true; return nil }).Scoped()
if err := app.Validate(di.Provided[*valReq]()).Err(); err != nil || built {
t.Fatalf("err=%v built=%v", err, built)
}
}
type valSrc struct{}
type valScopedSvc struct{ src *valSrc }
type valSingleton struct{ svc *valScopedSvc }
// A valid graph can visit one Scoped binding in two scopes: from a child,
// the scoped service needs a source the child provides, whose constructor
// needs a root singleton, which needs the same scoped service built in the
// root, which needs the root's source. Run time resolves it, since a path
// node is a binding in a holder; Validate compared bindings alone and called
// it a cycle. (issue 35)
func TestValidateTellsScopedInstancesApartByHolder(t *testing.T) {
app := di.New()
app.Value(&valSrc{})
app.Wire[*valScopedSvc](func(s *valSrc) *valScopedSvc { return &valScopedSvc{s} }).Scoped()
app.Wire[*valSingleton](func(s *valScopedSvc) *valSingleton { return &valSingleton{s} })
child := app.Child("child")
child.Wire[*valSrc](func(s *valSingleton) *valSrc { return s.svc.src })
if err := child.Validate().Err(); err != nil {
t.Fatalf("a graph the runtime resolves must validate: %v", err)
}
if _, err := child.Resolve[*valScopedSvc](); err != nil {
t.Fatalf("runtime: %v", err)
}
// The same binding twice under one holder is still a cycle.
cyc := di.New()
cyc.Wire[*valScopedSvc](func(*valSingleton) *valScopedSvc { return nil }).Scoped()
cyc.Wire[*valSingleton](func(*valScopedSvc) *valSingleton { return nil }).Scoped()
if !errors.Is(cyc.Validate().Err(), di.ErrCycle) {
t.Fatal("a real cycle among scoped bindings must still be reported")
}
}
// Needs brings the two parameters a closure used to hide into the declared
// graph: an optional one is no failure when nothing provides it, and a group
// parameter is walked member by member. (fx review)
type valRoute struct{}
type valTracer struct{}
type valRouter struct {
routes []valRoute
tracer *valTracer
}
func newValRouter(rs []valRoute, t *valTracer) *valRouter { return &valRouter{rs, t} }
func TestValidateOptionalIsNoFailure(t *testing.T) {
s := di.New()
s.Wire[*valRouter](newValRouter).Needs(di.AllOf[valRoute](), di.Optional[*valTracer]())
v := s.Validate()
if v.Err() != nil {
t.Fatalf("an optional nothing provides is not an error: %v", v.Err())
}
if len(v.Unchecked) != 0 {
t.Fatalf("the constructor is declared, so nothing is unchecked: %v", v.Unchecked)
}
}
// A group parameter is checked through its members, so a member that cannot
// be built is reported even though the group itself is fine. (fx review)
func TestValidateWalksAGroupParameter(t *testing.T) {
s := di.New()
s.Wire[valRoute](func(*valDB) valRoute { return valRoute{} }).Group() // *valDB is missing
s.Wire[*valRouter](newValRouter).Needs(di.AllOf[valRoute](), di.Optional[*valTracer]())
v := s.Validate()
if len(v.Errors) != 1 || !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("the member's own missing dependency should be reported: %v", v.Errors)
}
if !strings.Contains(v.Err().Error(), "valDB") {
t.Fatalf("the report should name what is missing: %v", v.Err())
}
}
// A cycle through a group parameter is still a cycle. (fx review)
func TestValidateFindsACycleThroughAGroup(t *testing.T) {
s := di.New()
s.Wire[valRoute](func(*valRouter) valRoute { return valRoute{} }).Group()
s.Wire[*valRouter](newValRouter).Needs(di.AllOf[valRoute](), di.Optional[*valTracer]())
v := s.Validate()
if !errors.Is(v.Err(), di.ErrCycle) {
t.Fatalf("want a cycle, got %v", v.Err())
}
}
// An optional that something does provide is walked like any dependency, so
// what it needs is still checked. (fx review)
func TestValidateWalksAProvidedOptional(t *testing.T) {
s := di.New()
s.Wire[*valTracer](func(*valDB) *valTracer { return nil }) // *valDB is missing
s.Wire[*valRouter](newValRouter).Needs(di.AllOf[valRoute](), di.Optional[*valTracer]())
if v := s.Validate(); !errors.Is(v.Err(), di.ErrNotProvided) {
t.Fatalf("the optional's own dependency should be checked: %v", v.Err())
}
}