From c4ac94f8f2de8da5c4a65ccc41e782cd4757fa83 Mon Sep 17 00:00:00 2001 From: Axel Christ Date: Wed, 23 Feb 2022 10:28:31 +0100 Subject: [PATCH] Implement testutils testutils contain utilities often used when writing tests interfacing with Kubernets. For now, the new utilities provided are: * EqualitiesEqualMatcher, providing equality matching based on semantic equality * EqualitiesDerivativeMatcher, providing derivative matching based on semantic equality * ErrorFuncMatcher, providing matching based on an error func. Most prominent usage will be with functions like apierrors.IsNotFound --- testutils/matchers/matchers.go | 112 +++++++++++++++++++ testutils/matchers/matchers_suite_test.go | 13 +++ testutils/matchers/matchers_test.go | 124 ++++++++++++++++++++++ testutils/testutils.go | 68 ++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 testutils/matchers/matchers.go create mode 100644 testutils/matchers/matchers_suite_test.go create mode 100644 testutils/matchers/matchers_test.go create mode 100644 testutils/testutils.go diff --git a/testutils/matchers/matchers.go b/testutils/matchers/matchers.go new file mode 100644 index 0000000..55213a5 --- /dev/null +++ b/testutils/matchers/matchers.go @@ -0,0 +1,112 @@ +package matchers + +import ( + "fmt" + "reflect" + "runtime" + + "github.com/onsi/gomega/format" + "k8s.io/utils/semantic" +) + +// EqualitiesEqualMatcher is a matcher that matches the Expected value using the given Equalities +// and semantic.Equalities.DeepEqual. +type EqualitiesEqualMatcher struct { + Equalities semantic.Equalities + Expected interface{} +} + +func (m *EqualitiesEqualMatcher) FailureMessage(actual interface{}) (message string) { + actualString, actualOK := actual.(string) + expectedString, expectedOK := m.Expected.(string) + if actualOK && expectedOK { + return format.MessageWithDiff(actualString, "to equal", expectedString) + } + + return format.Message(actual, "to equal with equality", m.Expected) +} + +func (m *EqualitiesEqualMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal with equality", m.Expected) +} + +func (m *EqualitiesEqualMatcher) Match(actual interface{}) (bool, error) { + if m.Equalities == nil { + return false, fmt.Errorf("must set Equalities") + } + + if actual == nil && m.Expected == nil { + return false, fmt.Errorf("refusing to compare to , BeNil() should be used instead") + } + + return m.Equalities.DeepEqual(actual, m.Expected), nil +} + +// EqualitiesDerivativeMatcher is a matcher that matches the Expected value using the given Equalities +// and semantic.Equalities.DeepDerivative. +type EqualitiesDerivativeMatcher struct { + Equalities semantic.Equalities + Expected interface{} +} + +func (m *EqualitiesDerivativeMatcher) FailureMessage(actual interface{}) (message string) { + actualString, actualOK := actual.(string) + expectedString, expectedOK := m.Expected.(string) + if actualOK && expectedOK { + return format.MessageWithDiff(actualString, "to derive", expectedString) + } + + return format.Message(actual, "to derive with equality", m.Expected) +} + +func (m *EqualitiesDerivativeMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to derive with equality", m.Expected) +} + +func (m *EqualitiesDerivativeMatcher) Match(actual interface{}) (bool, error) { + if m.Equalities == nil { + return false, fmt.Errorf("must set Equalities") + } + + if actual == nil && m.Expected == nil { + return false, fmt.Errorf("refusing to compare to , BeNil() should be used instead") + } + + return m.Equalities.DeepDerivative(actual, m.Expected), nil +} + +type ErrorFuncMatcher struct { + Name string + Func func(err error) bool +} + +func (m *ErrorFuncMatcher) Match(actual interface{}) (success bool, err error) { + if m.Func == nil { + return false, fmt.Errorf("must set Func") + } + + actualErr, ok := actual.(error) + if !ok { + return false, fmt.Errorf("expected an error-type but got %s", format.Object(actual, 0)) + } + + return m.Func(actualErr), nil +} + +func (m *ErrorFuncMatcher) nameOrFuncName() string { + if m.Name != "" { + return m.Name + } + + return runtime.FuncForPC(reflect.ValueOf(m.Func).Pointer()).Name() +} + +func (m *ErrorFuncMatcher) FailureMessage(actual interface{}) (message string) { + name := m.nameOrFuncName() + return fmt.Sprintf("expected an error matching %s to have occurred but got %s", name, format.Object(actual, 0)) +} + +func (m *ErrorFuncMatcher) NegatedFailureMessage(actual interface{}) (message string) { + name := m.nameOrFuncName() + return fmt.Sprintf("expected an error not matching %s to have occurred but got %s", name, format.Object(actual, 0)) +} diff --git a/testutils/matchers/matchers_suite_test.go b/testutils/matchers/matchers_suite_test.go new file mode 100644 index 0000000..385108d --- /dev/null +++ b/testutils/matchers/matchers_suite_test.go @@ -0,0 +1,13 @@ +package matchers_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestMatchers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Matchers Suite") +} diff --git a/testutils/matchers/matchers_test.go b/testutils/matchers/matchers_test.go new file mode 100644 index 0000000..6f2a3d8 --- /dev/null +++ b/testutils/matchers/matchers_test.go @@ -0,0 +1,124 @@ +package matchers_test + +import ( + "fmt" + + . "github.com/onmetal/controller-utils/testutils/matchers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/semantic" +) + +var _ = Describe("Matchers", func() { + Context("EqualitiesEqualMatcher", func() { + Describe("Match", func() { + It("should match using the supplied equalities", func() { + matcher := EqualitiesEqualMatcher{ + Equalities: semantic.EqualitiesOrDie(func(s1 string, s2 string) bool { + if s1 == "*" || s2 == "*" { + return true + } + return s1 == s2 + }), + Expected: "foo", + } + + Expect(matcher.Match("*")).To(BeTrue(), "* should match") + Expect(matcher.Match("foo")).To(BeTrue(), "foo should match") + Expect(matcher.Match("bar")).To(BeFalse(), "bar should not match") + Expect(matcher.Match(fmt.Errorf("foo"))).To(BeFalse(), "an error should not match") + }) + + It("should error if the equalities are not set", func() { + matcher := EqualitiesEqualMatcher{ + Expected: "foo", + } + _, err := matcher.Match("foo") + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("EqualitiesDerivativeMatcher", func() { + type Struct struct { + A string + B string + } + + Describe("Match", func() { + It("should match using the supplied equalities", func() { + matcher := EqualitiesDerivativeMatcher{ + Equalities: semantic.EqualitiesOrDie(func(s1 string, s2 string) bool { + if s1 == "" || s1 == "*" || s2 == "*" { + return true + } + return s1 == s2 + }), + Expected: Struct{ + A: "foo", + B: "bar", + }, + } + + Expect(matcher.Match(Struct{A: "*"})).To(BeTrue(), "A:* should match") + Expect(matcher.Match(Struct{A: "foo"})).To(BeTrue(), "A:foo should match") + Expect(matcher.Match(Struct{A: "foo", B: "bar"})).To(BeTrue(), "A:foo,B:bar should match") + Expect(matcher.Match(Struct{A: "bar"})).To(BeFalse(), "A:bar should not match") + Expect(matcher.Match(fmt.Errorf("foo"))).To(BeFalse(), "an error should not match") + }) + + It("should error if the equalities are not set", func() { + matcher := EqualitiesDerivativeMatcher{ + Expected: "foo", + } + _, err := matcher.Match("foo") + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("ErrorFuncMatcher", func() { + Describe("Match", func() { + It("should match using the given function", func() { + matcher := ErrorFuncMatcher{ + Func: apierrors.IsNotFound, + } + + Expect(matcher.Match(fmt.Errorf("custom"))).To(BeFalse(), "custom error should not match") + Expect(matcher.Match(apierrors.NewNotFound(schema.GroupResource{}, ""))).To(BeTrue(), "not found should match") + _, err := matcher.Match(1) + Expect(err).To(HaveOccurred()) + }) + + It("should error if the error function is not set", func() { + matcher := ErrorFuncMatcher{} + _, err := matcher.Match(fmt.Errorf("custom")) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("ErrorMessage", func() { + It("should report a correct error message", func() { + matcher := ErrorFuncMatcher{ + Func: apierrors.IsNotFound, + } + + Expect(matcher.FailureMessage(fmt.Errorf("custom"))). + To(HavePrefix("expected an error matching k8s.io/apimachinery/pkg/api/errors.IsNotFound to have occurred but got")) + }) + }) + + Describe("NegatedErrorMessage", func() { + It("should report a correct negated error message", func() { + matcher := ErrorFuncMatcher{ + Func: apierrors.IsNotFound, + } + + Expect(matcher.NegatedFailureMessage(fmt.Errorf("custom"))). + To(HavePrefix("expected an error not matching k8s.io/apimachinery/pkg/api/errors.IsNotFound to have occurred but got")) + }) + }) + }) +}) diff --git a/testutils/testutils.go b/testutils/testutils.go new file mode 100644 index 0000000..2e8c231 --- /dev/null +++ b/testutils/testutils.go @@ -0,0 +1,68 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "github.com/onmetal/controller-utils/testutils/matchers" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/utils/semantic" +) + +// EqualWithEquality returns a matcher that determines whether the expected value is equal to an actual +// value using the supplied semantic.Equalities. +func EqualWithEquality(equalities semantic.Equalities, expected interface{}) matchers.EqualitiesEqualMatcher { + return matchers.EqualitiesEqualMatcher{ + Equalities: equalities, + Expected: expected, + } +} + +// SemanticEqual returns a matcher that determines whether the expected value is equal to an actual value +// using equality.Semantic.Equalities. +func SemanticEqual(expected interface{}) matchers.EqualitiesEqualMatcher { + return EqualWithEquality(semantic.Equalities(equality.Semantic.Equalities), expected) +} + +// DerivativeWithEquality returns a matcher that determines whether the actual value derives from the expected +// value using the supplied semantic.Equalities. +func DerivativeWithEquality(equalities semantic.Equalities, expected interface{}) matchers.EqualitiesDerivativeMatcher { + return matchers.EqualitiesDerivativeMatcher{ + Equalities: equalities, + Expected: expected, + } +} + +// SemanticDerivative returns a matcher that determines whether the actual value derives from the expected +// value using equality.Semantic.Equalities. +func SemanticDerivative(expected interface{}) matchers.EqualitiesDerivativeMatcher { + return DerivativeWithEquality(semantic.Equalities(equality.Semantic.Equalities), expected) +} + +// MatchErrorFunc returns a matcher that determines whether the actual value is an error and matches the supplied +// function. The name of the function will be dynamically inferred. +func MatchErrorFunc(f func(err error) bool) matchers.ErrorFuncMatcher { + return matchers.ErrorFuncMatcher{ + Func: f, + } +} + +// MatchNamedErrorFunc returns a matcher that determines whether the actual value is an error and matches the supplied +// function. The given name will be used unless it's empty in which case it will be dynamically inferred. +func MatchNamedErrorFunc(name string, f func(err error) bool) matchers.ErrorFuncMatcher { + return matchers.ErrorFuncMatcher{ + Name: name, + Func: f, + } +}