diff --git a/pkg/envtest/komega/equalobject.go b/pkg/envtest/komega/equalobject.go index dec272d07e..eae6f65179 100644 --- a/pkg/envtest/komega/equalobject.go +++ b/pkg/envtest/komega/equalobject.go @@ -17,7 +17,6 @@ package komega import ( "fmt" "reflect" - "strconv" "strings" "github.com/google/go-cmp/cmp" @@ -33,15 +32,15 @@ var ( // IgnoreAutogeneratedMetadata contains the paths for all the metadata fields that are commonly set by the // client and APIServer. This is used as a MatchOption for situations when only user-provided metadata is relevant. IgnoreAutogeneratedMetadata = IgnorePaths{ - {"ObjectMeta", "UID"}, - {"ObjectMeta", "Generation"}, - {"ObjectMeta", "CreationTimestamp"}, - {"ObjectMeta", "ResourceVersion"}, - {"ObjectMeta", "ManagedFields"}, - {"ObjectMeta", "DeletionGracePeriodSeconds"}, - {"ObjectMeta", "DeletionTimestamp"}, - {"ObjectMeta", "SelfLink"}, - {"ObjectMeta", "GenerateName"}, + "metadata.uid", + "metadata.generation", + "metadata.creationTimestamp", + "metadata.resourceVersion", + "metadata.managedFields", + "metadata.deletionGracePeriodSeconds", + "metadata.deletionTimestamp", + "metadata.selfLink", + "metadata.generateName", } ) @@ -112,76 +111,67 @@ func (d diffPath) String() string { // diffReporter is a custom recorder for cmp.Diff which records all paths that are // different between two objects. type diffReporter struct { - stack []cmp.PathStep - path []string - jsonPath []string + stack []cmp.PathStep diffPaths []diffPath } func (r *diffReporter) PushStep(s cmp.PathStep) { r.stack = append(r.stack, s) - if len(r.stack) <= 1 { - return - } - switch s := s.(type) { - case cmp.SliceIndex: - r.path = append(r.path, strconv.Itoa(s.Key())) - r.jsonPath = append(r.jsonPath, strconv.Itoa(s.Key())) - case cmp.MapIndex: - key := fmt.Sprintf("%v", s.Key()) - // if strings.ContainsAny(key, ".[]/\\") { - // key = fmt.Sprintf("[%s]", key) - // } else { - // key = "." + key - // } - r.path = append(r.path, key) - r.jsonPath = append(r.jsonPath, key) - case cmp.StructField: - field := r.stack[len(r.stack)-2].Type().Field(s.Index()) - jsonName := strings.Split(field.Tag.Get("json"), ",")[0] - r.path = append(r.path, s.String()[1:]) - r.jsonPath = append(r.jsonPath, jsonName) - } } func (r *diffReporter) Report(res cmp.Result) { if !res.Equal() { - r.diffPaths = append(r.diffPaths, diffPath{types: r.path, json: r.jsonPath}) + r.diffPaths = append(r.diffPaths, r.currentPath()) } } -// func (r *diffReporter) currPath() string { -// p := []string{} -// for _, s := range r.stack[1:] { -// switch s := s.(type) { -// case cmp.StructField, cmp.SliceIndex, cmp.MapIndex: -// p = append(p, s.String()) -// } -// } -// return strings.Join(p, "")[1:] -// } +// currentPath converts the current stack into string representations that match +// the IgnorePaths and MatchPaths syntax. +func (r *diffReporter) currentPath() diffPath { + p := diffPath{types: []string{""}, json: []string{""}} + for si, s := range r.stack[1:] { + switch s := s.(type) { + case cmp.StructField: + p.types = append(p.types, s.String()[1:]) + // fetch the type information from the parent struct. + // Note: si has an offset of 1 compared to r.stack as we loop over r.stack[1:], so we don't need -1 + field := r.stack[si].Type().Field(s.Index()) + p.json = append(p.json, strings.Split(field.Tag.Get("json"), ",")[0]) + case cmp.SliceIndex: + key := fmt.Sprintf("[%d]", s.Key()) + p.types[len(p.types)-1] += key + p.json[len(p.json)-1] += key + case cmp.MapIndex: + key := fmt.Sprintf("%v", s.Key()) + if strings.ContainsAny(key, ".[]/\\") { + key = fmt.Sprintf("[%s]", key) + p.types[len(p.types)-1] += key + p.json[len(p.json)-1] += key + } else { + p.types = append(p.types, key) + p.json = append(p.json, key) + } + } + } + // Empty strings were added as the first element. If they're still empty, remove them again. + if len(p.json) > 0 && len(p.json[0]) == 0 { + p.json = p.json[1:] + p.types = p.types[1:] + } + return p +} func (r *diffReporter) PopStep() { - popped := r.stack[len(r.stack)-1] r.stack = r.stack[:len(r.stack)-1] - if _, ok := popped.(cmp.Indirect); ok { - return - } - if len(r.stack) <= 1 { - return - } - switch popped.(type) { - case cmp.SliceIndex, cmp.MapIndex, cmp.StructField: - r.path = r.path[:len(r.path)-1] - r.jsonPath = r.jsonPath[:len(r.jsonPath)-1] - } } // calculateDiff calculates the difference between two objects and returns the // paths of the fields that do not match. func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath { var original interface{} = m.original + // Remove the wrapping Object from unstructured.Unstructured to make comparison behave similar to + // regular objects. if u, isUnstructured := actual.(*unstructured.Unstructured); isUnstructured { actual = u.Object } @@ -196,33 +186,47 @@ func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath { // filterDiffPaths filters the diff paths using the paths in EqualObjectOptions. func filterDiffPaths(opts EqualObjectOptions, paths []diffPath) []diffPath { result := []diffPath{} - for _, c := range paths { - if len(opts.matchPaths) > 0 && (!matchesAnyPath(c.types, opts.matchPaths) || !matchesAnyPath(c.json, opts.matchPaths)) { + + for _, p := range paths { + if len(opts.matchPaths) > 0 && !hasAnyPathPrefix(p, opts.matchPaths) { continue } - if matchesAnyPath(c.types, opts.ignorePaths) || matchesAnyPath(c.json, opts.ignorePaths) { + if hasAnyPathPrefix(p, opts.ignorePaths) { continue } - result = append(result, c) + + result = append(result, p) } + return result } -func matchesPath(path []string, prefix []string) bool { +// hasPathPrefix compares the segments of a path +func hasPathPrefix(path []string, prefix []string) bool { for i, p := range prefix { - if i >= len(path) || p != path[i] { + if i >= len(path) { + return false + } + // return false if a segment doesn't match + if path[i] != p && (i < len(prefix)-1 || !segmentHasPrefix(path[i], p)) { return false } } return true } -// matchesAnyPath returns true if path matches any of the path prefixes. +func segmentHasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[0:len(prefix)] == prefix && + // if it is a prefix match, make sure the next character is a [ for array/map access + (len(s) == len(prefix) || s[len(prefix)] == '[') +} + +// hasAnyPathPrefix returns true if path matches any of the path prefixes. // It respects the name boundaries within paths, so 'ObjectMeta.Name' does not // match 'ObjectMeta.Namespace' for example. -func matchesAnyPath(path []string, prefixes [][]string) bool { +func hasAnyPathPrefix(path diffPath, prefixes [][]string) bool { for _, prefix := range prefixes { - if matchesPath(path, prefix) { + if hasPathPrefix(path.types, prefix) || hasPathPrefix(path.json, prefix) { return true } } @@ -249,23 +253,46 @@ func (o *EqualObjectOptions) ApplyOptions(opts []EqualObjectOption) *EqualObject return o } -// func parsePath(path string) []string { -// s := strings.Split(path, ".") -// return s -// } - // IgnorePaths instructs the Matcher to ignore given paths when computing a diff. -type IgnorePaths [][]string +// Paths are written in a syntax similar to Go with a few special cases. Both types and +// json/yaml field names are supported. +// +// Regular Paths +// "ObjectMeta.Name" +// "metadata.name" +// Arrays +// "metadata.ownerReferences[0].name" +// Maps, if they do not contain any of .[]/\ +// "metadata.labels.something" +// Maps, if they contain any of .[]/\ +// "metadata.labels[kubernetes.io/something]" +type IgnorePaths []string // ApplyToEqualObjectMatcher applies this configuration to the given MatchOptions. func (i IgnorePaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) { - opts.ignorePaths = append(opts.ignorePaths, i...) + for _, p := range i { + opts.ignorePaths = append(opts.ignorePaths, strings.Split(p, ".")) + } } // MatchPaths instructs the Matcher to restrict its diff to the given paths. If empty the Matcher will look at all paths. -type MatchPaths [][]string +// Paths are written in a syntax similar to Go with a few special cases. Both types and +// json/yaml field names are supported. +// +// Regular Paths +// "ObjectMeta.Name" +// "metadata.name" +// Arrays +// "metadata.ownerReferences[0].name" +// Maps, if they do not contain any of .[]/\ +// "metadata.labels.something" +// Maps, if they contain any of .[]/\ +// "metadata.labels[kubernetes.io/something]" +type MatchPaths []string // ApplyToEqualObjectMatcher applies this configuration to the given MatchOptions. func (i MatchPaths) ApplyToEqualObjectMatcher(opts *EqualObjectOptions) { - opts.matchPaths = append(opts.matchPaths, i...) + for _, p := range i { + opts.matchPaths = append(opts.ignorePaths, strings.Split(p, ".")) + } } diff --git a/pkg/envtest/komega/equalobject_test.go b/pkg/envtest/komega/equalobject_test.go index e675e82f70..9682b2df7c 100644 --- a/pkg/envtest/komega/equalobject_test.go +++ b/pkg/envtest/komega/equalobject_test.go @@ -12,43 +12,43 @@ import ( func TestEqualObjectMatcher(t *testing.T) { cases := []struct { - desc string - expected client.Object - actual client.Object - opts []EqualObjectOption - result bool + name string + original client.Object + modified client.Object + options []EqualObjectOption + want bool }{ { - desc: "succeed with equal objects", - expected: &appsv1.Deployment{ + name: "succeed with equal objects", + original: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, - actual: &appsv1.Deployment{ + modified: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, - result: true, + want: true, }, { - desc: "fail with non equal objects", - expected: &appsv1.Deployment{ + name: "fail with non equal objects", + original: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, }, - actual: &appsv1.Deployment{ + modified: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "somethingelse", }, }, - result: false, + want: false, }, { - desc: "succeeds if ignored fields do not match", - expected: &appsv1.Deployment{ + name: "succeeds if ignored fields do not match", + original: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{"somelabel": "somevalue"}, @@ -57,7 +57,7 @@ func TestEqualObjectMatcher(t *testing.T) { }}, }, }, - actual: &appsv1.Deployment{ + modified: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "somethingelse", Labels: map[string]string{"somelabel": "anothervalue"}, @@ -66,20 +66,20 @@ func TestEqualObjectMatcher(t *testing.T) { }}, }, }, - result: true, - opts: []EqualObjectOption{ + want: true, + options: []EqualObjectOption{ IgnorePaths{ - {"ObjectMeta", "Name"}, - {"ObjectMeta", "CreationTimestamp"}, - {"ObjectMeta", "Labels", "somelabel"}, - {"ObjectMeta", "OwnerReferences", "0", "Name"}, - {"Spec", "Template", "ObjectMeta"}, + "ObjectMeta.Name", + "ObjectMeta.CreationTimestamp", + "ObjectMeta.Labels.somelabel", + "ObjectMeta.OwnerReferences[0].Name", + "Spec.Template.ObjectMeta", }, }, }, { - desc: "succeeds if ignored fields in json notation do not match", - expected: &appsv1.Deployment{ + name: "succeeds if ignored fields in json notation do not match", + original: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{"somelabel": "somevalue"}, @@ -88,7 +88,7 @@ func TestEqualObjectMatcher(t *testing.T) { }}, }, }, - actual: &appsv1.Deployment{ + modified: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "somethingelse", Labels: map[string]string{"somelabel": "anothervalue"}, @@ -97,69 +97,550 @@ func TestEqualObjectMatcher(t *testing.T) { }}, }, }, - result: true, - opts: []EqualObjectOption{ + want: true, + options: []EqualObjectOption{ IgnorePaths{ - {"metadata", "name"}, - {"metadata", "creationTimestamp"}, - {"metadata", "labels", "somelabel"}, - {"metadata", "ownerReferences", "0", "name"}, - {"spec", "template", "metadata"}, + "metadata.name", + "metadata.creationTimestamp", + "metadata.labels.somelabel", + "metadata.ownerReferences[0].name", + "spec.template.metadata", }, }, }, { - desc: "succeeds if all allowed fields match, and some others do not", - expected: &appsv1.Deployment{ + name: "succeeds if all allowed fields match, and some others do not", + original: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", }, }, - actual: &appsv1.Deployment{ + modified: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "special", }, }, - result: true, - opts: []EqualObjectOption{ + want: true, + options: []EqualObjectOption{ MatchPaths{ - {"ObjectMeta", "Name"}, + "ObjectMeta.Name", }, }, }, { - desc: "works with unstructured.Unstructured", - expected: &unstructured.Unstructured{ + name: "works with unstructured.Unstructured", + original: &unstructured.Unstructured{ Object: map[string]interface{}{ "metadata": map[string]interface{}{ - "name": "something", + "name": "something", + "namespace": "test", }, }, }, - actual: &unstructured.Unstructured{ + modified: &unstructured.Unstructured{ Object: map[string]interface{}{ "metadata": map[string]interface{}{ - "name": "somethingelse", + "name": "somethingelse", + "namespace": "test", }, }, }, - result: true, - opts: []EqualObjectOption{ + want: true, + options: []EqualObjectOption{ IgnorePaths{ - {"metadata", "name"}, + "metadata.name", }, }, }, + + // Test when objects are equal. + { + name: "Equal field (spec) both in original and in modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + want: true, + }, + + { + name: "Equal nested field both in original and in modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + }, + }, + want: true, + }, + + // Test when there is a difference between the objects. + { + name: "Unequal field both in original and in modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar-changed", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + want: false, + }, + { + name: "Unequal nested field both in original and modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A-Changed", + }, + }, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + }, + }, + want: false, + }, + + { + name: "Value of type map with different values", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "map": map[string]string{ + "A": "A-changed", + "B": "B", + // C missing + }, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "map": map[string]string{ + "A": "A", + // B missing + "C": "C", + }, + }, + }, + }, + want: false, + }, + + { + name: "Value of type Array or Slice with same length but different values", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "slice": []string{ + "D", + "C", + "B", + }, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "slice": []string{ + "A", + "B", + "C", + }, + }, + }, + }, + want: false, + }, + + // This tests specific behaviour in how Kubernetes marshals the zero value of metav1.Time{}. + { + name: "Creation timestamp set to empty value on both original and modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + "metadata": map[string]interface{}{ + "selfLink": "foo", + "creationTimestamp": metav1.Time{}, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + "metadata": map[string]interface{}{ + "selfLink": "foo", + "creationTimestamp": metav1.Time{}, + }, + }, + }, + want: true, + }, + + // Cases to test diff when fields exist only in modified object. + { + name: "Field only in modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + want: false, + }, + { + name: "Nested field only in modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + }, + }, + want: false, + }, + { + name: "Creation timestamp exists on modified but not on original", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + "metadata": map[string]interface{}{ + "selfLink": "foo", + "creationTimestamp": "2021-11-03T11:05:17Z", + }, + }, + }, + want: false, + }, + + // Test when fields exists only in the original object. + { + name: "Field only in original", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + want: false, + }, + { + name: "Nested field only in original", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + want: false, + }, + { + name: "Creation timestamp exists on original but not on modified", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + "metadata": map[string]interface{}{ + "selfLink": "foo", + "creationTimestamp": "2021-11-03T11:05:17Z", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + + want: false, + }, + + // Test metadata fields computed by the system or in status are compared. + { + name: "Unequal Metadata fields computed by the system or in status", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "selfLink": "foo", + "uid": "foo", + "resourceVersion": "foo", + "generation": "foo", + "managedFields": "foo", + }, + "status": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + want: false, + }, + { + name: "Unequal labels and annotations", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "foo": "bar", + }, + "annotations": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + }, + want: false, + }, + + // Ignore fields MatchOption + // { + // name: "Unequal metadata fields ignored by IgnorePaths MatchOption", + // original: &unstructured.Unstructured{ + // Object: map[string]interface{}{}, + // }, + // modified: &unstructured.Unstructured{ + // Object: map[string]interface{}{ + // "metadata": map[string]interface{}{ + // "selfLink": "foo", + // "uid": "foo", + // "resourceVersion": "foo", + // "generation": "foo", + // "managedFields": "foo", + // }, + // }, + // }, + // options: []EqualObjectOption{IgnoreAutogeneratedMetadata}, + // want: true, + // }, + // { + // name: "Unequal labels and annotations ignored by IgnorePaths MatchOption", + // original: &unstructured.Unstructured{ + // Object: map[string]interface{}{}, + // }, + // modified: &unstructured.Unstructured{ + // Object: map[string]interface{}{ + // "metadata": map[string]interface{}{ + // "labels": map[string]interface{}{ + // "foo": "bar", + // }, + // "annotations": map[string]interface{}{ + // "foo": "bar", + // }, + // }, + // }, + // }, + // options: []EqualObjectOption{IgnorePaths{"metadata.labels", "metadata.annotations"}}, + // want: true, + // }, + // { + // name: "Ignore fields are not compared", + // original: &unstructured.Unstructured{ + // Object: map[string]interface{}{}, + // }, + // modified: &unstructured.Unstructured{ + // Object: map[string]interface{}{ + // "spec": map[string]interface{}{ + // "controlPlaneEndpoint": map[string]interface{}{ + // "host": "", + // "port": 0, + // }, + // }, + // }, + // }, + // options: []EqualObjectOption{IgnorePaths{"spec.controlPlaneEndpoint"}}, + // want: true, + // }, + { + name: "Ignore fields are not compared", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "ignored": "somevalue", + "superflous": "shouldcausefailure", + }, + }, + }, + }, + options: []EqualObjectOption{IgnorePaths{"metadata.annotations.ignored"}}, + want: false, + }, + + // AllowPaths MatchOption + { + name: "Unequal metadata fields not compared by setting AllowPaths MatchOption", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + }, + "metadata": map[string]interface{}{ + "selfLink": "foo", + "uid": "foo", + }, + }, + }, + options: []EqualObjectOption{MatchPaths{"spec"}}, + want: true, + }, + + // More tests + { + name: "No changes", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + "B": "B", + "C": "C", // C only in modified + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + "B": "B", + }, + }, + }, + want: false, + }, + { + name: "Many changes", + original: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + // B missing + "C": "C", // C only in modified + }, + }, + }, + modified: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "A": "A", + "B": "B", + }, + }, + }, + want: false, + }, } for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { + t.Run(c.name, func(t *testing.T) { g := NewWithT(t) - m := EqualObject(c.expected, c.opts...) - g.Expect(m.Match(c.actual)).To(Equal(c.result)) - //fmt.Println(m.FailureMessage(&c.actual)) + m := EqualObject(c.original, c.options...) + success, _ := m.Match(c.modified) + if !success { + t.Log(m.FailureMessage(c.modified)) + } + g.Expect(success).To(Equal(c.want)) }) } }