Skip to content

Commit 9708172

Browse files
committed
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
1 parent 3a2218e commit 9708172

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

testutils/matchers/matchers.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package matchers
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"runtime"
7+
8+
"github.com/onsi/gomega/format"
9+
"k8s.io/utils/semantic"
10+
)
11+
12+
// EqualitiesEqualMatcher is a matcher that matches the Expected value using the given Equalities
13+
// and semantic.Equalities.DeepEqual.
14+
type EqualitiesEqualMatcher struct {
15+
Equalities semantic.Equalities
16+
Expected interface{}
17+
}
18+
19+
func (m *EqualitiesEqualMatcher) FailureMessage(actual interface{}) (message string) {
20+
actualString, actualOK := actual.(string)
21+
expectedString, expectedOK := m.Expected.(string)
22+
if actualOK && expectedOK {
23+
return format.MessageWithDiff(actualString, "to equal", expectedString)
24+
}
25+
26+
return format.Message(actual, "to equal with equality", m.Expected)
27+
}
28+
29+
func (m *EqualitiesEqualMatcher) NegatedFailureMessage(actual interface{}) (message string) {
30+
return format.Message(actual, "not to equal with equality", m.Expected)
31+
}
32+
33+
func (m *EqualitiesEqualMatcher) Match(actual interface{}) (bool, error) {
34+
if m.Equalities == nil {
35+
return false, fmt.Errorf("must set Equalities")
36+
}
37+
38+
if actual == nil && m.Expected == nil {
39+
return false, fmt.Errorf("refusing to compare <nil> to <nil>, BeNil() should be used instead")
40+
}
41+
42+
return m.Equalities.DeepEqual(actual, m.Expected), nil
43+
}
44+
45+
// EqualitiesDerivativeMatcher is a matcher that matches the Expected value using the given Equalities
46+
// and semantic.Equalities.DeepDerivative.
47+
type EqualitiesDerivativeMatcher struct {
48+
Equalities semantic.Equalities
49+
Expected interface{}
50+
}
51+
52+
func (m *EqualitiesDerivativeMatcher) FailureMessage(actual interface{}) (message string) {
53+
actualString, actualOK := actual.(string)
54+
expectedString, expectedOK := m.Expected.(string)
55+
if actualOK && expectedOK {
56+
return format.MessageWithDiff(actualString, "to derive", expectedString)
57+
}
58+
59+
return format.Message(actual, "to derive with equality", m.Expected)
60+
}
61+
62+
func (m *EqualitiesDerivativeMatcher) NegatedFailureMessage(actual interface{}) (message string) {
63+
return format.Message(actual, "not to derive with equality", m.Expected)
64+
}
65+
66+
func (m *EqualitiesDerivativeMatcher) Match(actual interface{}) (bool, error) {
67+
if m.Equalities == nil {
68+
return false, fmt.Errorf("must set Equalities")
69+
}
70+
71+
if actual == nil && m.Expected == nil {
72+
return false, fmt.Errorf("refusing to compare <nil> to <nil>, BeNil() should be used instead")
73+
}
74+
75+
return m.Equalities.DeepDerivative(actual, m.Expected), nil
76+
}
77+
78+
type ErrorFuncMatcher struct {
79+
Name string
80+
Func func(err error) bool
81+
}
82+
83+
func (m *ErrorFuncMatcher) Match(actual interface{}) (success bool, err error) {
84+
if m.Func == nil {
85+
return false, fmt.Errorf("must set Func")
86+
}
87+
88+
actualErr, ok := actual.(error)
89+
if !ok {
90+
return false, fmt.Errorf("expected an error-type but got %s", format.Object(actual, 0))
91+
}
92+
93+
return m.Func(actualErr), nil
94+
}
95+
96+
func (m *ErrorFuncMatcher) nameOrFuncName() string {
97+
if m.Name != "" {
98+
return m.Name
99+
}
100+
101+
return runtime.FuncForPC(reflect.ValueOf(m.Func).Pointer()).Name()
102+
}
103+
104+
func (m *ErrorFuncMatcher) FailureMessage(actual interface{}) (message string) {
105+
name := m.nameOrFuncName()
106+
return fmt.Sprintf("expected an error matching %s to have occurred but got %s", name, format.Object(actual, 0))
107+
}
108+
109+
func (m *ErrorFuncMatcher) NegatedFailureMessage(actual interface{}) (message string) {
110+
name := m.nameOrFuncName()
111+
return fmt.Sprintf("expected an error not matching %s to have occurred but got %s", name, format.Object(actual, 0))
112+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package matchers_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestMatchers(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Matchers Suite")
13+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package matchers_test
2+
3+
import (
4+
"fmt"
5+
6+
. "github.com/onmetal/controller-utils/testutils/matchers"
7+
. "github.com/onsi/ginkgo"
8+
. "github.com/onsi/gomega"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
"k8s.io/utils/semantic"
12+
)
13+
14+
var _ = Describe("Matchers", func() {
15+
Context("EqualitiesEqualMatcher", func() {
16+
Describe("Match", func() {
17+
It("should match using the supplied equalities", func() {
18+
matcher := EqualitiesEqualMatcher{
19+
Equalities: semantic.EqualitiesOrDie(func(s1 string, s2 string) bool {
20+
if s1 == "*" || s2 == "*" {
21+
return true
22+
}
23+
return s1 == s2
24+
}),
25+
Expected: "foo",
26+
}
27+
28+
Expect(matcher.Match("*")).To(BeTrue(), "* should match")
29+
Expect(matcher.Match("foo")).To(BeTrue(), "foo should match")
30+
Expect(matcher.Match("bar")).To(BeFalse(), "bar should not match")
31+
Expect(matcher.Match(fmt.Errorf("foo"))).To(BeFalse(), "an error should not match")
32+
})
33+
34+
It("should error if the equalities are not set", func() {
35+
matcher := EqualitiesEqualMatcher{
36+
Expected: "foo",
37+
}
38+
_, err := matcher.Match("foo")
39+
Expect(err).To(HaveOccurred())
40+
})
41+
})
42+
})
43+
44+
Context("EqualitiesDerivativeMatcher", func() {
45+
type Struct struct {
46+
A string
47+
B string
48+
}
49+
50+
Describe("Match", func() {
51+
It("should match using the supplied equalities", func() {
52+
matcher := EqualitiesDerivativeMatcher{
53+
Equalities: semantic.EqualitiesOrDie(func(s1 string, s2 string) bool {
54+
if s1 == "" || s1 == "*" || s2 == "*" {
55+
return true
56+
}
57+
return s1 == s2
58+
}),
59+
Expected: Struct{
60+
A: "foo",
61+
B: "bar",
62+
},
63+
}
64+
65+
Expect(matcher.Match(Struct{A: "*"})).To(BeTrue(), "A:* should match")
66+
Expect(matcher.Match(Struct{A: "foo"})).To(BeTrue(), "A:foo should match")
67+
Expect(matcher.Match(Struct{A: "foo", B: "bar"})).To(BeTrue(), "A:foo,B:bar should match")
68+
Expect(matcher.Match(Struct{A: "bar"})).To(BeFalse(), "A:bar should not match")
69+
Expect(matcher.Match(fmt.Errorf("foo"))).To(BeFalse(), "an error should not match")
70+
})
71+
72+
It("should error if the equalities are not set", func() {
73+
matcher := EqualitiesDerivativeMatcher{
74+
Expected: "foo",
75+
}
76+
_, err := matcher.Match("foo")
77+
Expect(err).To(HaveOccurred())
78+
})
79+
})
80+
})
81+
82+
Context("ErrorFuncMatcher", func() {
83+
Describe("Match", func() {
84+
It("should match using the given function", func() {
85+
matcher := ErrorFuncMatcher{
86+
Func: apierrors.IsNotFound,
87+
}
88+
89+
Expect(matcher.Match(fmt.Errorf("custom"))).To(BeFalse(), "custom error should not match")
90+
Expect(matcher.Match(apierrors.NewNotFound(schema.GroupResource{}, ""))).To(BeTrue(), "not found should match")
91+
_, err := matcher.Match(1)
92+
Expect(err).To(HaveOccurred())
93+
})
94+
95+
It("should error if the error function is not set", func() {
96+
matcher := ErrorFuncMatcher{}
97+
_, err := matcher.Match(fmt.Errorf("custom"))
98+
Expect(err).To(HaveOccurred())
99+
})
100+
})
101+
102+
Describe("ErrorMessage", func() {
103+
It("should report a correct error message", func() {
104+
matcher := ErrorFuncMatcher{
105+
Func: apierrors.IsNotFound,
106+
}
107+
108+
Expect(matcher.FailureMessage(fmt.Errorf("custom"))).
109+
To(HavePrefix("expected an error matching k8s.io/apimachinery/pkg/api/errors.IsNotFound to have occurred but got"))
110+
})
111+
})
112+
113+
Describe("NegatedErrorMessage", func() {
114+
It("should report a correct negated error message", func() {
115+
matcher := ErrorFuncMatcher{
116+
Func: apierrors.IsNotFound,
117+
}
118+
119+
Expect(matcher.NegatedFailureMessage(fmt.Errorf("custom"))).
120+
To(HavePrefix("expected an error not matching k8s.io/apimachinery/pkg/api/errors.IsNotFound to have occurred but got"))
121+
})
122+
})
123+
})
124+
})

testutils/testutils.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package testutils
2+
3+
import (
4+
"github.com/onmetal/controller-utils/testutils/matchers"
5+
"k8s.io/apimachinery/pkg/api/equality"
6+
"k8s.io/utils/semantic"
7+
)
8+
9+
// EqualWithEquality returns a matcher that determines whether the expected value is equal to an actual
10+
// value using the supplied semantic.Equalities.
11+
func EqualWithEquality(equalities semantic.Equalities, expected interface{}) matchers.EqualitiesEqualMatcher {
12+
return matchers.EqualitiesEqualMatcher{
13+
Equalities: equalities,
14+
Expected: expected,
15+
}
16+
}
17+
18+
// SemanticEqual returns a matcher that determines whether the expected value is equal to an actual value
19+
// using equality.Semantic.Equalities.
20+
func SemanticEqual(expected interface{}) matchers.EqualitiesEqualMatcher {
21+
return EqualWithEquality(semantic.Equalities(equality.Semantic.Equalities), expected)
22+
}
23+
24+
// DerivativeWithEquality returns a matcher that determines whether the actual value derives from the expected
25+
// value using the supplied semantic.Equalities.
26+
func DerivativeWithEquality(equalities semantic.Equalities, expected interface{}) matchers.EqualitiesDerivativeMatcher {
27+
return matchers.EqualitiesDerivativeMatcher{
28+
Equalities: equalities,
29+
Expected: expected,
30+
}
31+
}
32+
33+
// SemanticDerivative returns a matcher that determines whether the actual value derives from the expected
34+
// value using equality.Semantic.Equalities.
35+
func SemanticDerivative(expected interface{}) matchers.EqualitiesDerivativeMatcher {
36+
return DerivativeWithEquality(semantic.Equalities(equality.Semantic.Equalities), expected)
37+
}
38+
39+
// MatchErrorFunc returns a matcher that determines whether the actual value is an error and matches the supplied
40+
// function. The name of the function will be dynamically inferred.
41+
func MatchErrorFunc(f func(err error) bool) matchers.ErrorFuncMatcher {
42+
return matchers.ErrorFuncMatcher{
43+
Func: f,
44+
}
45+
}
46+
47+
// MatchNamedErrorFunc returns a matcher that determines whether the actual value is an error and matches the supplied
48+
// function. The given name will be used unless it's empty in which case it will be dynamically inferred.
49+
func MatchNamedErrorFunc(name string, f func(err error) bool) matchers.ErrorFuncMatcher {
50+
return matchers.ErrorFuncMatcher{
51+
Name: name,
52+
Func: f,
53+
}
54+
}

0 commit comments

Comments
 (0)