1
14
15 package komega
16
17 import (
18 "fmt"
19 "reflect"
20 "strings"
21
22 "github.com/google/go-cmp/cmp"
23 "github.com/onsi/gomega/format"
24 "github.com/onsi/gomega/types"
25 "k8s.io/apimachinery/pkg/runtime"
26 )
27
28
29
30 var (
31
32
33 IgnoreAutogeneratedMetadata = IgnorePaths{
34 "metadata.uid",
35 "metadata.generation",
36 "metadata.creationTimestamp",
37 "metadata.resourceVersion",
38 "metadata.managedFields",
39 "metadata.deletionGracePeriodSeconds",
40 "metadata.deletionTimestamp",
41 "metadata.selfLink",
42 "metadata.generateName",
43 }
44 )
45
46 type diffPath struct {
47 types []string
48 json []string
49 }
50
51
52 type equalObjectMatcher struct {
53
54 original runtime.Object
55
56
57 diffPaths []diffPath
58
59
60 options *EqualObjectOptions
61 }
62
63
64
65 func EqualObject(original runtime.Object, opts ...EqualObjectOption) types.GomegaMatcher {
66 matchOptions := &EqualObjectOptions{}
67 matchOptions = matchOptions.ApplyOptions(opts)
68
69 return &equalObjectMatcher{
70 options: matchOptions,
71 original: original,
72 }
73 }
74
75
76
77 func (m *equalObjectMatcher) Match(actual interface{}) (success bool, err error) {
78
79
80
81 actualIsNil := reflect.ValueOf(actual).IsNil()
82 originalIsNil := reflect.ValueOf(m.original).IsNil()
83
84 if actualIsNil && originalIsNil {
85 return true, nil
86 }
87 if actualIsNil || originalIsNil {
88 return false, fmt.Errorf("can not compare an object with a nil. original %v , actual %v", m.original, actual)
89 }
90
91 m.diffPaths = m.calculateDiff(actual)
92 return len(m.diffPaths) == 0, nil
93 }
94
95
96 func (m *equalObjectMatcher) FailureMessage(actual interface{}) (message string) {
97 return fmt.Sprintf("the following fields were expected to match but did not:\n%v\n%s", m.diffPaths,
98 format.Message(actual, "expected to match", m.original))
99 }
100
101
102 func (m *equalObjectMatcher) NegatedFailureMessage(actual interface{}) (message string) {
103 return "it was expected that some fields do not match, but all of them did"
104 }
105
106 func (d diffPath) String() string {
107 return fmt.Sprintf("(%s/%s)", strings.Join(d.types, "."), strings.Join(d.json, "."))
108 }
109
110
111
112 type diffReporter struct {
113 stack []cmp.PathStep
114
115 diffPaths []diffPath
116 }
117
118 func (r *diffReporter) PushStep(s cmp.PathStep) {
119 r.stack = append(r.stack, s)
120 }
121
122 func (r *diffReporter) Report(res cmp.Result) {
123 if !res.Equal() {
124 r.diffPaths = append(r.diffPaths, r.currentPath())
125 }
126 }
127
128
129
130 func (r *diffReporter) currentPath() diffPath {
131 p := diffPath{types: []string{""}, json: []string{""}}
132 for si, s := range r.stack[1:] {
133 switch s := s.(type) {
134 case cmp.StructField:
135 p.types = append(p.types, s.String()[1:])
136
137
138 field := r.stack[si].Type().Field(s.Index())
139 p.json = append(p.json, strings.Split(field.Tag.Get("json"), ",")[0])
140 case cmp.SliceIndex:
141 key := fmt.Sprintf("[%d]", s.Key())
142 p.types[len(p.types)-1] += key
143 p.json[len(p.json)-1] += key
144 case cmp.MapIndex:
145 key := fmt.Sprintf("%v", s.Key())
146 if strings.ContainsAny(key, ".[]/\\") {
147 key = fmt.Sprintf("[%s]", key)
148 p.types[len(p.types)-1] += key
149 p.json[len(p.json)-1] += key
150 } else {
151 p.types = append(p.types, key)
152 p.json = append(p.json, key)
153 }
154 }
155 }
156
157 if len(p.json) > 0 && len(p.json[0]) == 0 {
158 p.json = p.json[1:]
159 p.types = p.types[1:]
160 }
161 return p
162 }
163
164 func (r *diffReporter) PopStep() {
165 r.stack = r.stack[:len(r.stack)-1]
166 }
167
168
169
170 func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath {
171 var original interface{} = m.original
172
173
174 if u, isUnstructured := actual.(runtime.Unstructured); isUnstructured {
175 actual = u.UnstructuredContent()
176 }
177 if u, ok := m.original.(runtime.Unstructured); ok {
178 original = u.UnstructuredContent()
179 }
180 r := diffReporter{}
181 cmp.Diff(original, actual, cmp.Reporter(&r))
182 return filterDiffPaths(*m.options, r.diffPaths)
183 }
184
185
186 func filterDiffPaths(opts EqualObjectOptions, paths []diffPath) []diffPath {
187 result := []diffPath{}
188
189 for _, p := range paths {
190 if len(opts.matchPaths) > 0 && !hasAnyPathPrefix(p, opts.matchPaths) {
191 continue
192 }
193 if hasAnyPathPrefix(p, opts.ignorePaths) {
194 continue
195 }
196
197 result = append(result, p)
198 }
199
200 return result
201 }
202
203
204 func hasPathPrefix(path []string, prefix []string) bool {
205 for i, p := range prefix {
206 if i >= len(path) {
207 return false
208 }
209
210 if path[i] != p && (i < len(prefix)-1 || !segmentHasPrefix(path[i], p)) {
211 return false
212 }
213 }
214 return true
215 }
216
217 func segmentHasPrefix(s, prefix string) bool {
218 return len(s) >= len(prefix) && s[0:len(prefix)] == prefix &&
219
220 (len(s) == len(prefix) || s[len(prefix)] == '[')
221 }
222
223
224
225
226 func hasAnyPathPrefix(path diffPath, prefixes [][]string) bool {
227 for _, prefix := range prefixes {
228 if hasPathPrefix(path.types, prefix) || hasPathPrefix(path.json, prefix) {
229 return true
230 }
231 }
232 return false
233 }
234
235
236 type EqualObjectOption interface {
237
238 ApplyToEqualObjectMatcher(options *EqualObjectOptions)
239 }
240
241
242 type EqualObjectOptions struct {
243 ignorePaths [][]string
244 matchPaths [][]string
245 }
246
247
248 func (o *EqualObjectOptions) ApplyOptions(opts []EqualObjectOption) *EqualObjectOptions {
249 for _, opt := range opts {
250 opt.ApplyToEqualObjectMatcher(o)
251 }
252 return o
253 }
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268 type IgnorePaths []string
269
270
271 func (i IgnorePaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) {
272 for _, p := range i {
273 opts.ignorePaths = append(opts.ignorePaths, strings.Split(p, "."))
274 }
275 }
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290 type MatchPaths []string
291
292
293 func (i MatchPaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) {
294 for _, p := range i {
295 opts.matchPaths = append(opts.ignorePaths, strings.Split(p, "."))
296 }
297 }
298
View as plain text