1
16
17 package framework
18
19 import (
20 "fmt"
21 "path"
22 "reflect"
23 "regexp"
24 "slices"
25 "strings"
26
27 "github.com/onsi/ginkgo/v2"
28 "github.com/onsi/ginkgo/v2/types"
29
30 apierrors "k8s.io/apimachinery/pkg/api/errors"
31 "k8s.io/apimachinery/pkg/util/sets"
32 utilfeature "k8s.io/apiserver/pkg/util/feature"
33 "k8s.io/component-base/featuregate"
34 )
35
36
37
38 type Feature string
39
40
41
42 type Environment string
43
44
45
46
47 type NodeFeature string
48
49 type Valid[T comparable] struct {
50 items sets.Set[T]
51 frozen bool
52 }
53
54
55
56
57
58
59
60
61 func (v *Valid[T]) Add(item T) T {
62 if v.frozen {
63 RecordBug(NewBug(fmt.Sprintf(`registry %T is already frozen, "%v" must not be added anymore`, *v, item), 1))
64 }
65 if v.items == nil {
66 v.items = sets.New[T]()
67 }
68 if v.items.Has(item) {
69 RecordBug(NewBug(fmt.Sprintf(`registry %T already contains "%v", it must not be added again`, *v, item), 1))
70 }
71 v.items.Insert(item)
72 return item
73 }
74
75 func (v *Valid[T]) Freeze() {
76 v.frozen = true
77 }
78
79
80
81
82
83 var (
84 ValidFeatures Valid[Feature]
85 ValidEnvironments Valid[Environment]
86 ValidNodeFeatures Valid[NodeFeature]
87 )
88
89 var errInterface = reflect.TypeOf((*error)(nil)).Elem()
90
91
92
93
94
95
96 func IgnoreNotFound(in any) any {
97 inType := reflect.TypeOf(in)
98 inValue := reflect.ValueOf(in)
99 return reflect.MakeFunc(inType, func(args []reflect.Value) []reflect.Value {
100 out := inValue.Call(args)
101 if len(out) > 0 {
102 lastValue := out[len(out)-1]
103 last := lastValue.Interface()
104 if last != nil && lastValue.Type().Implements(errInterface) && apierrors.IsNotFound(last.(error)) {
105 out[len(out)-1] = reflect.Zero(errInterface)
106 }
107 }
108 return out
109 }).Interface()
110 }
111
112
113
114
115 func AnnotatedLocation(annotation string) types.CodeLocation {
116 return AnnotatedLocationWithOffset(annotation, 1)
117 }
118
119
120
121 func AnnotatedLocationWithOffset(annotation string, offset int) types.CodeLocation {
122 codeLocation := types.NewCodeLocation(offset + 1)
123 codeLocation.FileName = path.Base(codeLocation.FileName)
124 codeLocation = types.NewCustomCodeLocation(annotation + " | " + codeLocation.String())
125 return codeLocation
126 }
127
128
129
130
131 func SIGDescribe(sig string) func(...interface{}) bool {
132 if !sigRE.MatchString(sig) || strings.HasPrefix(sig, "sig-") {
133 RecordBug(NewBug(fmt.Sprintf("SIG label must be lowercase, no spaces and no sig- prefix, got instead: %q", sig), 1))
134 }
135 return func(args ...interface{}) bool {
136 args = append([]interface{}{WithLabel("sig-" + sig)}, args...)
137 return registerInSuite(ginkgo.Describe, args)
138 }
139 }
140
141 var sigRE = regexp.MustCompile(`^[a-z]+(-[a-z]+)*$`)
142
143
144 func ConformanceIt(args ...interface{}) bool {
145 args = append(args, ginkgo.Offset(1), WithConformance())
146 return It(args...)
147 }
148
149
150
151
152
153
154
155 func It(args ...interface{}) bool {
156 return registerInSuite(ginkgo.It, args)
157 }
158
159
160 func (f *Framework) It(args ...interface{}) bool {
161 return registerInSuite(ginkgo.It, args)
162 }
163
164
165
166
167
168
169
170 func Describe(args ...interface{}) bool {
171 return registerInSuite(ginkgo.Describe, args)
172 }
173
174
175 func (f *Framework) Describe(args ...interface{}) bool {
176 return registerInSuite(ginkgo.Describe, args)
177 }
178
179
180
181
182
183
184
185 func Context(args ...interface{}) bool {
186 return registerInSuite(ginkgo.Context, args)
187 }
188
189
190 func (f *Framework) Context(args ...interface{}) bool {
191 return registerInSuite(ginkgo.Context, args)
192 }
193
194
195
196 func registerInSuite(ginkgoCall func(string, ...interface{}) bool, args []interface{}) bool {
197 var ginkgoArgs []interface{}
198 var offset ginkgo.Offset
199 var texts []string
200
201 addLabel := func(label string) {
202 texts = append(texts, fmt.Sprintf("[%s]", label))
203 ginkgoArgs = append(ginkgoArgs, ginkgo.Label(label))
204 }
205
206 haveEmptyStrings := false
207 for _, arg := range args {
208 switch arg := arg.(type) {
209 case label:
210 fullLabel := strings.Join(arg.parts, ":")
211 addLabel(fullLabel)
212 if arg.extra != "" {
213 addLabel(arg.extra)
214 }
215 if fullLabel == "Serial" {
216 ginkgoArgs = append(ginkgoArgs, ginkgo.Serial)
217 }
218 case ginkgo.Offset:
219 offset = arg
220 case string:
221 if arg == "" {
222 haveEmptyStrings = true
223 }
224 texts = append(texts, arg)
225 default:
226 ginkgoArgs = append(ginkgoArgs, arg)
227 }
228 }
229 offset += 2
230
231
232 if haveEmptyStrings {
233 RecordBug(NewBug("empty strings as separators are unnecessary and need to be removed", int(offset)))
234 }
235
236
237
238 for _, text := range texts {
239 if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
240 RecordBug(NewBug(fmt.Sprintf("trailing or leading spaces are unnecessary and need to be removed: %q", text), int(offset)))
241 }
242 }
243
244 ginkgoArgs = append(ginkgoArgs, offset)
245 text := strings.Join(texts, " ")
246 return ginkgoCall(text, ginkgoArgs...)
247 }
248
249 var (
250 tagRe = regexp.MustCompile(`\[.*?\]`)
251 deprecatedTags = sets.New("Conformance", "Flaky", "NodeConformance", "Disruptive", "Serial", "Slow")
252 deprecatedTagPrefixes = sets.New("Environment", "Feature", "NodeFeature", "FeatureGate")
253 deprecatedStability = sets.New("Alpha", "Beta")
254 )
255
256
257 func validateSpecs(specs types.SpecReports) {
258 checked := sets.New[call]()
259
260 for _, spec := range specs {
261 for i, text := range spec.ContainerHierarchyTexts {
262 c := call{
263 text: text,
264 location: spec.ContainerHierarchyLocations[i],
265 }
266 if checked.Has(c) {
267
268 continue
269 }
270 checked.Insert(c)
271 validateText(c.location, text, spec.ContainerHierarchyLabels[i])
272 }
273 c := call{
274 text: spec.LeafNodeText,
275 location: spec.LeafNodeLocation,
276 }
277 if !checked.Has(c) {
278 validateText(spec.LeafNodeLocation, spec.LeafNodeText, spec.LeafNodeLabels)
279 checked.Insert(c)
280 }
281 }
282 }
283
284
285
286
287
288 type call struct {
289 text string
290 location types.CodeLocation
291 }
292
293
294
295
296 func validateText(location types.CodeLocation, text string, labels []string) {
297 for _, tag := range tagRe.FindAllString(text, -1) {
298 if tag == "[]" {
299 recordTextBug(location, "[] in plain text is invalid")
300 continue
301 }
302
303 tag = tag[1 : len(tag)-1]
304 if slices.Contains(labels, tag) {
305
306 continue
307 }
308 if deprecatedTags.Has(tag) {
309 recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added through With%s instead", tag, tag))
310 }
311 if deprecatedStability.Has(tag) {
312 recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added by defining the feature gate through WithFeatureGate instead", tag))
313 }
314 if index := strings.Index(tag, ":"); index > 0 {
315 prefix := tag[:index]
316 if deprecatedTagPrefixes.Has(prefix) {
317 recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added through With%s(%s) instead", tag, prefix, tag[index+1:]))
318 }
319 }
320 }
321 }
322
323 func recordTextBug(location types.CodeLocation, message string) {
324 RecordBug(Bug{FileName: location.FileName, LineNumber: location.LineNumber, Message: message})
325 }
326
327
328
329
330
331
332 func WithFeature(name Feature) interface{} {
333 return withFeature(name)
334 }
335
336
337 func (f *Framework) WithFeature(name Feature) interface{} {
338 return withFeature(name)
339 }
340
341 func withFeature(name Feature) interface{} {
342 if !ValidFeatures.items.Has(name) {
343 RecordBug(NewBug(fmt.Sprintf("WithFeature: unknown feature %q", name), 2))
344 }
345 return newLabel("Feature", string(name))
346 }
347
348
349
350
351
352
353
354
355
356 func WithFeatureGate(featureGate featuregate.Feature) interface{} {
357 return withFeatureGate(featureGate)
358 }
359
360
361 func (f *Framework) WithFeatureGate(featureGate featuregate.Feature) interface{} {
362 return withFeatureGate(featureGate)
363 }
364
365 func withFeatureGate(featureGate featuregate.Feature) interface{} {
366 spec, ok := utilfeature.DefaultMutableFeatureGate.GetAll()[featureGate]
367 if !ok {
368 RecordBug(NewBug(fmt.Sprintf("WithFeatureGate: the feature gate %q is unknown", featureGate), 2))
369 }
370
371
372 var level string
373 if spec.PreRelease != "" {
374 level = string(spec.PreRelease)
375 level = strings.ToUpper(level[0:1]) + strings.ToLower(level[1:])
376 }
377
378 l := newLabel("FeatureGate", string(featureGate))
379 l.extra = level
380 return l
381 }
382
383
384
385
386
387
388 func WithEnvironment(name Environment) interface{} {
389 return withEnvironment(name)
390 }
391
392
393 func (f *Framework) WithEnvironment(name Environment) interface{} {
394 return withEnvironment(name)
395 }
396
397 func withEnvironment(name Environment) interface{} {
398 if !ValidEnvironments.items.Has(name) {
399 RecordBug(NewBug(fmt.Sprintf("WithEnvironment: unknown environment %q", name), 2))
400 }
401 return newLabel("Environment", string(name))
402 }
403
404
405
406
407
408
409
410 func WithNodeFeature(name NodeFeature) interface{} {
411 return withNodeFeature(name)
412 }
413
414
415 func (f *Framework) WithNodeFeature(name NodeFeature) interface{} {
416 return withNodeFeature(name)
417 }
418
419 func withNodeFeature(name NodeFeature) interface{} {
420 if !ValidNodeFeatures.items.Has(name) {
421 RecordBug(NewBug(fmt.Sprintf("WithNodeFeature: unknown environment %q", name), 2))
422 }
423 return newLabel("NodeFeature", string(name))
424 }
425
426
427
428
429
430 func WithConformance() interface{} {
431 return withConformance()
432 }
433
434
435 func (f *Framework) WithConformance() interface{} {
436 return withConformance()
437 }
438
439 func withConformance() interface{} {
440 return newLabel("Conformance")
441 }
442
443
444
445
446
447 func WithNodeConformance() interface{} {
448 return withNodeConformance()
449 }
450
451
452 func (f *Framework) WithNodeConformance() interface{} {
453 return withNodeConformance()
454 }
455
456 func withNodeConformance() interface{} {
457 return newLabel("NodeConformance")
458 }
459
460
461
462
463
464 func WithDisruptive() interface{} {
465 return withDisruptive()
466 }
467
468
469 func (f *Framework) WithDisruptive() interface{} {
470 return withDisruptive()
471 }
472
473 func withDisruptive() interface{} {
474 return newLabel("Disruptive")
475 }
476
477
478
479
480
481
482
483
484 func WithSerial() interface{} {
485 return withSerial()
486 }
487
488
489 func (f *Framework) WithSerial() interface{} {
490 return withSerial()
491 }
492
493 func withSerial() interface{} {
494 return newLabel("Serial")
495 }
496
497
498
499
500 func WithSlow() interface{} {
501 return withSlow()
502 }
503
504
505 func (f *Framework) WithSlow() interface{} {
506 return WithSlow()
507 }
508
509 func withSlow() interface{} {
510 return newLabel("Slow")
511 }
512
513
514
515
516 func WithLabel(label string) interface{} {
517 return withLabel(label)
518 }
519
520
521 func (f *Framework) WithLabel(label string) interface{} {
522 return withLabel(label)
523 }
524
525 func withLabel(label string) interface{} {
526 return newLabel(label)
527 }
528
529
530
531 func WithFlaky() interface{} {
532 return withFlaky()
533 }
534
535
536 func (f *Framework) WithFlaky() interface{} {
537 return withFlaky()
538 }
539
540 func withFlaky() interface{} {
541 return newLabel("Flaky")
542 }
543
544 type label struct {
545
546 parts []string
547
548 extra string
549
550
551
552 explanation string
553 }
554
555 func newLabel(parts ...string) label {
556 return label{
557 parts: parts,
558 explanation: "If you see this as part of an 'Unknown Decorator' error from Ginkgo, then you need to replace the ginkgo.It/Context/Describe call with the corresponding framework.It/Context/Describe or (if available) f.It/Context/Describe.",
559 }
560 }
561
562
563
564
565
566 func TagsEqual(a, b interface{}) bool {
567 al, ok := a.(label)
568 if !ok {
569 return false
570 }
571 bl, ok := b.(label)
572 if !ok {
573 return false
574 }
575 if al.extra != bl.extra {
576 return false
577 }
578 return slices.Equal(al.parts, bl.parts)
579 }
580
View as plain text