mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Add a reflection based object diff with structured format
This commit is contained in:
parent
404a1b1dc5
commit
4c6da96f85
@ -20,10 +20,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StringDiff diffs a and b and returns a human readable diff.
|
// StringDiff diffs a and b and returns a human readable diff.
|
||||||
@ -74,6 +78,148 @@ func ObjectGoPrintDiff(a, b interface{}) string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ObjectReflectDiff(a, b interface{}) string {
|
||||||
|
vA, vB := reflect.ValueOf(a), reflect.ValueOf(b)
|
||||||
|
if vA.Type() != vB.Type() {
|
||||||
|
return fmt.Sprintf("type A %T and type B %T do not match", a, b)
|
||||||
|
}
|
||||||
|
diffs := objectReflectDiff(field.NewPath("object"), vA, vB)
|
||||||
|
if len(diffs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
out := []string{""}
|
||||||
|
for _, d := range diffs {
|
||||||
|
out = append(out,
|
||||||
|
fmt.Sprintf("%s:", d.path),
|
||||||
|
limit(fmt.Sprintf(" a: %#v", d.a), 80),
|
||||||
|
limit(fmt.Sprintf(" b: %#v", d.b), 80),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return strings.Join(out, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func limit(s string, max int) string {
|
||||||
|
if len(s) > max {
|
||||||
|
return s[:max]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func public(s string) bool {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s[:1] == strings.ToUpper(s[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
type diff struct {
|
||||||
|
path *field.Path
|
||||||
|
a, b interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderedDiffs []diff
|
||||||
|
|
||||||
|
func (d orderedDiffs) Len() int { return len(d) }
|
||||||
|
func (d orderedDiffs) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
||||||
|
func (d orderedDiffs) Less(i, j int) bool {
|
||||||
|
a, b := d[i].path.String(), d[j].path.String()
|
||||||
|
if a < b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectReflectDiff(path *field.Path, a, b reflect.Value) []diff {
|
||||||
|
switch a.Type().Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
var changes []diff
|
||||||
|
for i := 0; i < a.Type().NumField(); i++ {
|
||||||
|
if !public(a.Type().Field(i).Name) {
|
||||||
|
if reflect.DeepEqual(a.Interface(), b.Interface()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []diff{{path: path, a: fmt.Sprintf("%#v", a), b: fmt.Sprintf("%#v", b)}}
|
||||||
|
}
|
||||||
|
if sub := objectReflectDiff(path.Child(a.Type().Field(i).Name), a.Field(i), b.Field(i)); len(sub) > 0 {
|
||||||
|
changes = append(changes, sub...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
case reflect.Ptr:
|
||||||
|
if a.IsNil() || b.IsNil() {
|
||||||
|
switch {
|
||||||
|
case a.IsNil() && b.IsNil():
|
||||||
|
return nil
|
||||||
|
case a.IsNil():
|
||||||
|
return []diff{{path: path, a: nil, b: b.Interface()}}
|
||||||
|
default:
|
||||||
|
return []diff{{path: path, a: a.Interface(), b: nil}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectReflectDiff(path, a.Elem(), b.Elem())
|
||||||
|
case reflect.Chan:
|
||||||
|
if !reflect.DeepEqual(a.Interface(), b.Interface()) {
|
||||||
|
return []diff{{path: path, a: a.Interface(), b: b.Interface()}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case reflect.Slice:
|
||||||
|
if reflect.DeepEqual(a, b) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lA, lB := a.Len(), b.Len()
|
||||||
|
l := lA
|
||||||
|
if lB < lA {
|
||||||
|
l = lB
|
||||||
|
}
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
if !reflect.DeepEqual(a.Index(i), b.Index(i)) {
|
||||||
|
return objectReflectDiff(path.Index(i), a.Index(i), b.Index(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var diffs []diff
|
||||||
|
for i := l; l < lA; i++ {
|
||||||
|
diffs = append(diffs, diff{path: path.Index(i), a: a.Index(i), b: nil})
|
||||||
|
}
|
||||||
|
for i := l; l < lB; i++ {
|
||||||
|
diffs = append(diffs, diff{path: path.Index(i), a: nil, b: b.Index(i)})
|
||||||
|
}
|
||||||
|
return diffs
|
||||||
|
case reflect.Map:
|
||||||
|
if reflect.DeepEqual(a, b) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
aKeys := make(map[interface{}]interface{})
|
||||||
|
for _, key := range a.MapKeys() {
|
||||||
|
aKeys[key.Interface()] = a.MapIndex(key).Interface()
|
||||||
|
}
|
||||||
|
var missing []diff
|
||||||
|
for _, key := range b.MapKeys() {
|
||||||
|
if _, ok := aKeys[key.Interface()]; ok {
|
||||||
|
delete(aKeys, key.Interface())
|
||||||
|
if reflect.DeepEqual(a.MapIndex(key).Interface(), b.MapIndex(key).Interface()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missing = append(missing, diff{path: path.Key(fmt.Sprintf("%s", key.Interface())), a: a.MapIndex(key).Interface(), b: b.MapIndex(key).Interface()})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missing = append(missing, diff{path: path.Key(fmt.Sprintf("%s", key.Interface())), a: nil, b: b.MapIndex(key).Interface()})
|
||||||
|
}
|
||||||
|
for key, value := range aKeys {
|
||||||
|
missing = append(missing, diff{path: path.Key(fmt.Sprintf("%s", key)), a: value, b: nil})
|
||||||
|
}
|
||||||
|
sort.Sort(orderedDiffs(missing))
|
||||||
|
return missing
|
||||||
|
default:
|
||||||
|
if reflect.DeepEqual(a.Interface(), b.Interface()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !a.CanInterface() {
|
||||||
|
return []diff{{path: path, a: fmt.Sprintf("%#v", a), b: fmt.Sprintf("%#v", b)}}
|
||||||
|
}
|
||||||
|
return []diff{{path: path, a: a.Interface(), b: b.Interface()}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
|
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
|
||||||
// enabling easy visual scanning for mismatches.
|
// enabling easy visual scanning for mismatches.
|
||||||
func ObjectGoPrintSideBySide(a, b interface{}) string {
|
func ObjectGoPrintSideBySide(a, b interface{}) string {
|
||||||
|
39
pkg/util/diff/diff_test.go
Normal file
39
pkg/util/diff/diff_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectReflectDiff(t *testing.T) {
|
||||||
|
expect := `
|
||||||
|
object[other]:
|
||||||
|
a: 2
|
||||||
|
b: <nil>
|
||||||
|
object[test]:
|
||||||
|
a: 1
|
||||||
|
b: 2
|
||||||
|
object[third]:
|
||||||
|
a: <nil>
|
||||||
|
b: 3`
|
||||||
|
a := map[string]int{"test": 1, "other": 2}
|
||||||
|
b := map[string]int{"test": 2, "third": 3}
|
||||||
|
if actual := ObjectReflectDiff(a, b); actual != expect {
|
||||||
|
t.Errorf("unexpected output: %s", actual)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user