Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions testutils/matchers/matchers.go
Original file line number Diff line number Diff line change
@@ -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 <nil> to <nil>, 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 <nil> to <nil>, 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))
}
13 changes: 13 additions & 0 deletions testutils/matchers/matchers_suite_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
124 changes: 124 additions & 0 deletions testutils/matchers/matchers_test.go
Original file line number Diff line number Diff line change
@@ -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"))
})
})
})
})
68 changes: 68 additions & 0 deletions testutils/testutils.go
Original file line number Diff line number Diff line change
@@ -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,
}
}