Skip to content

Commit 35b6b37

Browse files
committed
WIP: Update golden test framework
* Move to envtest by default (instead of mock-kubeapiserver) * Use a more convention env var * Support object rewriting
1 parent cfd0b42 commit 35b6b37

File tree

2 files changed

+106
-31
lines changed

2 files changed

+106
-31
lines changed

docs/addon/walkthrough/tests.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This means that the output is materialized and checked in to the repo; this
3333
proves to be very handy for understanding the impact of a change.
3434

3535
There's also a helpful "cheat" function, which rewrite the output when you run
36-
the tests locally - set the HACK_AUTOFIX_EXPECTED_OUTPUT env var to a non-empty
36+
the tests locally - set the WRITE_GOLDEN_OUTPUT env var to a non-empty
3737
string. This is useful when you have a big set of changes; it's just as easy to
3838
review the changes yourself in the diff and there's not a ton of value in typing
3939
them out.
@@ -87,7 +87,7 @@ the env-var cheat code.
8787
```bash
8888
cd pkg/controller/{{operator}}/tests
8989
touch tests/simple.out.yaml
90-
HACK_AUTOFIX_EXPECTED_OUTPUT=1 go test ./...
90+
WRITE_GOLDEN_OUTPUT=1 go test ./...
9191
```
9292

9393
1. Verify the output is reproducible

pkg/test/golden/validator.go

+104-29
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import (
2727
"strings"
2828

2929
"k8s.io/apimachinery/pkg/api/meta"
30+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3031
"k8s.io/apimachinery/pkg/runtime"
3132
"k8s.io/apimachinery/pkg/runtime/serializer/json"
3233
"k8s.io/apimachinery/pkg/types"
3334
"k8s.io/apimachinery/pkg/util/diff"
3435
"k8s.io/client-go/rest"
3536
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/envtest"
3638
"sigs.k8s.io/controller-runtime/pkg/manager"
37-
"sigs.k8s.io/controller-runtime/pkg/scheme"
3839
"sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver"
3940
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon"
4041
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/loaders"
@@ -61,37 +62,59 @@ type T interface {
6162
TempDir() string
6263
}
6364

64-
func NewValidator(t T, b *scheme.Builder) *validator {
65+
type AddToSchemeFunc func(s *runtime.Scheme) error
66+
67+
func NewValidator(t T, env *envtest.Environment, addToSchemeFuncs ...AddToSchemeFunc) *validator {
68+
ctx := context.TODO()
69+
ctx, cancel := context.WithCancel(ctx)
70+
6571
v := &validator{T: t, scheme: runtime.NewScheme()}
66-
if err := b.AddToScheme(v.scheme); err != nil {
67-
t.Fatalf("error from AddToScheme: %v", err)
72+
for _, addToSchemeFunc := range addToSchemeFuncs {
73+
if err := addToSchemeFunc(v.scheme); err != nil {
74+
t.Fatalf("error from AddToScheme: %v", err)
75+
}
6876
}
6977

7078
v.T.Helper()
7179
addon.Init()
7280
v.findChannelsPath()
7381

74-
k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0")
75-
if err != nil {
76-
t.Fatalf("error building mock kube-apiserver: %v", err)
77-
}
82+
useEnvtest := true
83+
var restConfig *rest.Config
84+
if useEnvtest {
85+
rc, err := env.Start()
86+
if err != nil {
87+
t.Fatalf("failed to start envtest kube-apiserver: %v", err)
88+
}
89+
restConfig = rc
90+
t.Cleanup(func() {
91+
if err := env.Stop(); err != nil {
92+
t.Errorf("error stopping envtest: %v", err)
93+
}
94+
})
7895

79-
addr, err := k8s.StartServing()
80-
if err != nil {
81-
t.Errorf("error starting mock kube-apiserver: %v", err)
82-
}
83-
v.k8s = k8s
96+
} else {
97+
k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0")
98+
if err != nil {
99+
t.Fatalf("error building mock kube-apiserver: %v", err)
100+
}
84101

85-
t.Cleanup(func() {
86-
if err := k8s.Stop(); err != nil {
87-
t.Errorf("error stopping mock kube-apiserver: %v", err)
102+
addr, err := k8s.StartServing()
103+
if err != nil {
104+
t.Errorf("error starting mock kube-apiserver: %v", err)
88105
}
89-
})
106+
v.k8s = k8s
90107

91-
restConfig := &rest.Config{
92-
Host: addr.String(),
93-
}
108+
t.Cleanup(func() {
109+
if err := k8s.Stop(); err != nil {
110+
t.Errorf("error stopping mock kube-apiserver: %v", err)
111+
}
112+
})
94113

114+
restConfig = &rest.Config{
115+
Host: addr.String(),
116+
}
117+
}
95118
mgr, err := manager.New(restConfig, manager.Options{
96119
Scheme: v.scheme,
97120
})
@@ -100,6 +123,25 @@ func NewValidator(t T, b *scheme.Builder) *validator {
100123
}
101124
v.client = mgr.GetClient()
102125
v.mgr = mgr
126+
127+
managerError := make(chan error)
128+
go func() {
129+
err := v.mgr.Start(ctx)
130+
managerError <- err
131+
}()
132+
133+
// Wait for the manager to start
134+
if !v.mgr.GetCache().WaitForCacheSync(ctx) {
135+
t.Fatalf("error waiting for cache sync")
136+
}
137+
138+
t.Cleanup(func() {
139+
// Cancel the context so the manager exits
140+
cancel()
141+
// Wait for manager to exit
142+
<-managerError
143+
})
144+
103145
return v
104146
}
105147

@@ -186,7 +228,13 @@ func (v *validator) Client() client.Client {
186228
return v.client
187229
}
188230

189-
func (v *validator) Validate(r declarative.Reconciler) {
231+
type ValidateOptions struct {
232+
RewriteObjects func(o *unstructured.Unstructured)
233+
}
234+
235+
func (v *validator) Validate(r *declarative.Reconciler, options ValidateOptions) {
236+
ctx := context.TODO()
237+
190238
t := v.T
191239
t.Helper()
192240

@@ -205,8 +253,6 @@ func (v *validator) Validate(r declarative.Reconciler) {
205253
t.Fatalf("error reading dir %s: %v", basedir, err)
206254
}
207255

208-
ctx := context.TODO()
209-
210256
for _, f := range files {
211257
p := filepath.Join(basedir, f.Name())
212258
t.Logf("Filepath: %s", p)
@@ -217,7 +263,7 @@ func (v *validator) Validate(r declarative.Reconciler) {
217263
}
218264

219265
if strings.HasSuffix(p, "~") {
220-
// Ignore editor temp files (for sanity)
266+
// Ignore editor temp files (this makes development easier)
221267
t.Logf("ignoring editor temp file %s", p)
222268
continue
223269
}
@@ -258,6 +304,7 @@ func (v *validator) Validate(r declarative.Reconciler) {
258304
if err := v.client.Create(ctx, obj); err != nil {
259305
t.Errorf("error creating resource in %s: %v", p, err)
260306
}
307+
t.Logf("created object %v %v/%v", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName())
261308
objectsToCleanup = append(objectsToCleanup, obj)
262309
}
263310
}
@@ -291,6 +338,15 @@ func (v *validator) Validate(r declarative.Reconciler) {
291338
continue
292339
}
293340

341+
{
342+
obj := cr.(client.Object)
343+
if err := v.client.Create(ctx, obj); err != nil {
344+
t.Errorf("error creating resource in %s: %v", p, err)
345+
}
346+
t.Logf("created object %v %v/%v", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName())
347+
objectsToCleanup = append(objectsToCleanup, obj)
348+
}
349+
294350
namespace, err := metadataAccessor.Namespace(cr)
295351
if err != nil {
296352
t.Errorf("error getting namespace in %s: %v", p, err)
@@ -324,6 +380,9 @@ func (v *validator) Validate(r declarative.Reconciler) {
324380
b.WriteString("---\n")
325381
}
326382
u := o.UnstructuredObject()
383+
if options.RewriteObjects != nil {
384+
options.RewriteObjects(u)
385+
}
327386
if err := yamlizer.Encode(u, &b); err != nil {
328387
t.Fatalf("error encoding to yaml: %v", err)
329388
}
@@ -336,15 +395,19 @@ func (v *validator) Validate(r declarative.Reconciler) {
336395
{
337396
b, err := os.ReadFile(expectedPath)
338397
if err != nil {
339-
t.Errorf("error reading file %s: %v", expectedPath, err)
340-
continue
398+
if os.IsNotExist(err) && ShouldWriteGoldenOutput(t) {
399+
// We'll create the file below
400+
} else {
401+
t.Errorf("error reading file %s: %v", expectedPath, err)
402+
continue
403+
}
341404
}
342405
expectedYAML = string(b)
343406
}
344407

345408
if actualYAML != expectedYAML {
346-
if os.Getenv("HACK_AUTOFIX_EXPECTED_OUTPUT") != "" {
347-
t.Logf("HACK_AUTOFIX_EXPECTED_OUTPUT is set; replacing expected output in %s", expectedPath)
409+
if ShouldWriteGoldenOutput(t) {
410+
t.Logf("WRITE_GOLDEN_OUTPUT is set; replacing expected output in %s", expectedPath)
348411
if err := os.WriteFile(expectedPath, []byte(actualYAML), 0644); err != nil {
349412
t.Fatalf("error writing expected output to %s: %v", expectedPath, err)
350413
}
@@ -357,15 +420,27 @@ func (v *validator) Validate(r declarative.Reconciler) {
357420
}
358421

359422
t.Errorf("unexpected diff between actual and expected YAML. See previous output for details.")
360-
t.Logf(`To regenerate the output based on this result, rerun this test with HACK_AUTOFIX_EXPECTED_OUTPUT="true"`)
423+
t.Logf(`To regenerate the output based on this result, rerun this test with WRITE_GOLDEN_OUTPUT="true"`)
361424
}
362425

363426
for _, objectToCleanup := range objectsToCleanup {
364427
if err := v.client.Delete(ctx, objectToCleanup); err != nil {
365428
t.Errorf("error deleting object: %v", err)
366429
}
367430
}
431+
432+
}
433+
}
434+
435+
func ShouldWriteGoldenOutput(t T) bool {
436+
if os.Getenv("HACK_AUTOFIX_EXPECTED_OUTPUT") != "" {
437+
t.Logf("HACK_AUTOFIX_EXPECTED_OUTPUT is set, please switch to use WRITE_GOLDEN_OUTPUT. This may be an test failure in future versions.")
438+
return true
439+
}
440+
if os.Getenv("WRITE_GOLDEN_OUTPUT") != "" {
441+
return true
368442
}
443+
return false
369444
}
370445

371446
func diffFiles(t T, expectedPath, actual string) error {

0 commit comments

Comments
 (0)