Skip to content

Commit

Permalink
Add side input for golden file tests
Browse files Browse the repository at this point in the history
Adds a posibility to provide a *.side_in.yaml file, that contains
resources that should exist before the resource defined in `*.in.yaml` is applied.
These side input resources are going to be created before running a testcase, and
cleaned up at the end of it. The resources will be accessible via Get and List.
Note that the existing functionality has not been changed.
  • Loading branch information
llamallamaduck committed Jan 26, 2022
1 parent fea7e5c commit 5ab7463
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 14 deletions.
7 changes: 6 additions & 1 deletion docs/addon/walkthrough/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ The bulk of these tests are implemented in `pkg/tests` via the `StandardValidato
The test scans a `tests` directory for `*.in.yaml` files, runs the operator for each
of them, and compares the output to `*.out.yaml` files.

It is also possible to provide `*.side_in.yaml` file that contains resources that
should exist before the resource defined in `*.in.yaml` is applied. These resources
are going to be created before running a test, and cleaned up upon finishing. They
will be accessible via Get and List.

We perform simple comparison-of-output tests, any changes
to the output are treated as test failures, and printed as a diff.
This means that the output is materialized and checked in to the repo; this
Expand Down Expand Up @@ -59,7 +64,7 @@ the env-var cheat code.
func TestController(t *testing.T) {
v := golden.NewValidator(t, api.SchemeBuilder)
dr := &{{operator}}Reconciler{
Client: v.Manager().GetClient(),
Client: v.Client(),
}
err := dr.setupReconciler(v.Manager())
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
func TestGuestbook(t *testing.T) {
v := golden.NewValidator(t, api.SchemeBuilder)
dr := &GuestbookReconciler{
Client: v.Manager().GetClient(),
Client: v.Client(),
}
err := dr.setupReconciler(v.Manager())
if err != nil {
Expand Down
47 changes: 42 additions & 5 deletions pkg/test/golden/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff"
clientScheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon"
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/loaders"
Expand All @@ -52,7 +51,8 @@ func NewValidator(t *testing.T, b *scheme.Builder) *validator {
addon.Init()
v.findChannelsPath()

v.mgr = mocks.NewManager(mocks.NewClient(clientScheme.Scheme))
v.client = mocks.NewClient(v.scheme)
v.mgr = mocks.NewManager(v.client)
v.mgr.Scheme = v.scheme
return v
}
Expand All @@ -62,6 +62,7 @@ type validator struct {
scheme *runtime.Scheme
TestDir string
mgr mocks.Manager
client mocks.FakeClient
}

// findChannelsPath will search for a channels directory, which is helpful when running under bazel
Expand Down Expand Up @@ -132,6 +133,10 @@ func (v *validator) Manager() *mocks.Manager {
return &v.mgr
}

func (v *validator) Client() *mocks.FakeClient {
return &v.client
}

func (v *validator) Validate(r declarative.Reconciler) {
t := v.T
t.Helper()
Expand Down Expand Up @@ -168,13 +173,41 @@ func (v *validator) Validate(r declarative.Reconciler) {
continue
}

// Ignore any files that are not input - we want to find .side_in and .out files
// that correspond to given .in file
if !strings.HasSuffix(p, ".in.yaml") {
if !strings.HasSuffix(p, ".out.yaml") {
t.Errorf("unexpected file in tests directory: %s", p)
}
continue
}

objectsToCleanup := []runtime.Object{}
// Check if there is a file containing side inputs for this test
sideInputPath := strings.Replace(p, ".in.yaml", ".side_in.yaml", -1)
sideInputRead, err := os.ReadFile(sideInputPath)
if err != nil {
t.Logf("Could not read side input file %s: %v, skipping", sideInputPath, err)
} else {
objs, err := manifest.ParseObjects(ctx, string(sideInputRead))
if err != nil {
t.Errorf("error parsing file %s: %v", p, err)
continue
}

for _, obj := range objs.Items {
json, err := obj.JSON()
if err != nil {
t.Errorf("error converting resource to json in %s: %v", p, err)
continue
}
decoded, _, err := serializer.Decode(json, nil, nil)
if err != nil {
t.Errorf("error parsing resource in %s: %v", p, err)
continue
}
v.client.CreateRuntimeObject(ctx, decoded)
objectsToCleanup = append(objectsToCleanup, decoded)
}
}

b, err := os.ReadFile(p)
if err != nil {
t.Errorf("error reading file %s: %v", p, err)
Expand Down Expand Up @@ -273,6 +306,10 @@ func (v *validator) Validate(r declarative.Reconciler) {
t.Logf(`To regenerate the output based on this result, rerun this test with HACK_AUTOFIX_EXPECTED_OUTPUT="true"`)
}

for _, objectToCleanup := range objectsToCleanup {
v.client.DeleteRuntimeObject(ctx, objectToCleanup)
}

}
}

Expand Down
62 changes: 55 additions & 7 deletions pkg/test/mocks/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,44 @@ func (f FakeClient) Get(ctx context.Context, key client.ObjectKey, out client.Ob
return err
}

func (FakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
panic("not implemented")
func (f FakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
gvkUnprocessed, err := apiutil.GVKForObject(list, f.scheme)
if err != nil {
return err
}
// this will be a SomethingList kind, and we want to get the Something: remove `List` (last 4 chars) of this string
nonListKind := gvkUnprocessed.Kind
nonListKind = nonListKind[:len(nonListKind)-4]
gvk := schema.GroupVersionKind{gvkUnprocessed.Group, gvkUnprocessed.Version, nonListKind}
gvr, _ := meta.UnsafeGuessKindToResource(gvk)

listObject, err := f.tracker.List(gvr, gvk, "")
if err != nil {
return err
}

items, err := meta.ExtractList(listObject)
if err != nil {
return err
}

return meta.SetList(list, items)
}

func (f FakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
gvr, err := getGVRFromObject(obj, f.scheme)
if err != nil {
return err
}
return f.create(gvr, obj, opts...)
}

func (f FakeClient) CreateRuntimeObject(ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error {
gvr := getGVRFromRuntimeObject(obj)
return f.create(gvr, obj, opts...)
}

func (f FakeClient) create(gvr schema.GroupVersionResource, obj runtime.Object, opts ...client.CreateOption) error {
createOptions := &client.CreateOptions{}
createOptions.ApplyOptions(opts)

Expand All @@ -60,19 +93,28 @@ func (f FakeClient) Create(ctx context.Context, obj client.Object, opts ...clien
}
}

gvr, err := getGVRFromObject(obj, f.scheme)
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
return f.tracker.Create(gvr, obj, accessor.GetNamespace())
}

func (f FakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
gvr, err := getGVRFromObject(obj, f.scheme)
if err != nil {
return err
}
return f.tracker.Create(gvr, obj, accessor.GetNamespace())
return f.tracker.Delete(gvr, obj.GetNamespace(), obj.GetName())
}

func (FakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
return nil
func (f FakeClient) DeleteRuntimeObject(ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error {
gvr := getGVRFromRuntimeObject(obj)
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
return f.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
}

func (FakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
Expand Down Expand Up @@ -108,6 +150,12 @@ func getGVRFromObject(obj client.Object, scheme *runtime.Scheme) (schema.GroupVe
return gvr, nil
}

func getGVRFromRuntimeObject(obj runtime.Object) schema.GroupVersionResource {
gvk := obj.GetObjectKind().GroupVersionKind()
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
return gvr
}

type FakeStatusClient struct{}

func (FakeStatusClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
Expand Down

0 comments on commit 5ab7463

Please sign in to comment.