314 lines
8.2 KiB
Go
314 lines
8.2 KiB
Go
|
/*
|
||
|
Copyright 2014 The Kubernetes 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 diff
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"text/tabwriter"
|
||
|
|
||
|
"github.com/davecgh/go-spew/spew"
|
||
|
|
||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||
|
)
|
||
|
|
||
|
// StringDiff diffs a and b and returns a human readable diff.
|
||
|
func StringDiff(a, b string) string {
|
||
|
ba := []byte(a)
|
||
|
bb := []byte(b)
|
||
|
out := []byte{}
|
||
|
i := 0
|
||
|
for ; i < len(ba) && i < len(bb); i++ {
|
||
|
if ba[i] != bb[i] {
|
||
|
break
|
||
|
}
|
||
|
out = append(out, ba[i])
|
||
|
}
|
||
|
out = append(out, []byte("\n\nA: ")...)
|
||
|
out = append(out, ba[i:]...)
|
||
|
out = append(out, []byte("\n\nB: ")...)
|
||
|
out = append(out, bb[i:]...)
|
||
|
out = append(out, []byte("\n\n")...)
|
||
|
return string(out)
|
||
|
}
|
||
|
|
||
|
// ObjectDiff writes the two objects out as JSON and prints out the identical part of
|
||
|
// the objects followed by the remaining part of 'a' and finally the remaining part of 'b'.
|
||
|
// For debugging tests.
|
||
|
func ObjectDiff(a, b interface{}) string {
|
||
|
ab, err := json.Marshal(a)
|
||
|
if err != nil {
|
||
|
panic(fmt.Sprintf("a: %v", err))
|
||
|
}
|
||
|
bb, err := json.Marshal(b)
|
||
|
if err != nil {
|
||
|
panic(fmt.Sprintf("b: %v", err))
|
||
|
}
|
||
|
return StringDiff(string(ab), string(bb))
|
||
|
}
|
||
|
|
||
|
// ObjectGoPrintDiff is like ObjectDiff, but uses go-spew to print the objects,
|
||
|
// which shows absolutely everything by recursing into every single pointer
|
||
|
// (go's %#v formatters OTOH stop at a certain point). This is needed when you
|
||
|
// can't figure out why reflect.DeepEqual is returning false and nothing is
|
||
|
// showing you differences. This will.
|
||
|
func ObjectGoPrintDiff(a, b interface{}) string {
|
||
|
s := spew.ConfigState{DisableMethods: true}
|
||
|
return StringDiff(
|
||
|
s.Sprintf("%#v", a),
|
||
|
s.Sprintf("%#v", b),
|
||
|
)
|
||
|
}
|
||
|
|
||
|
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 "<no diffs>"
|
||
|
}
|
||
|
out := []string{""}
|
||
|
for _, d := range diffs {
|
||
|
elidedA, elidedB := limit(d.a, d.b, 80)
|
||
|
out = append(out,
|
||
|
fmt.Sprintf("%s:", d.path),
|
||
|
fmt.Sprintf(" a: %s", elidedA),
|
||
|
fmt.Sprintf(" b: %s", elidedB),
|
||
|
)
|
||
|
}
|
||
|
return strings.Join(out, "\n")
|
||
|
}
|
||
|
|
||
|
// limit:
|
||
|
// 1. stringifies aObj and bObj
|
||
|
// 2. elides identical prefixes if either is too long
|
||
|
// 3. elides remaining content from the end if either is too long
|
||
|
func limit(aObj, bObj interface{}, max int) (string, string) {
|
||
|
elidedPrefix := ""
|
||
|
elidedASuffix := ""
|
||
|
elidedBSuffix := ""
|
||
|
a, b := fmt.Sprintf("%#v", aObj), fmt.Sprintf("%#v", bObj)
|
||
|
|
||
|
if aObj != nil && bObj != nil {
|
||
|
if aType, bType := fmt.Sprintf("%T", aObj), fmt.Sprintf("%T", bObj); aType != bType {
|
||
|
a = fmt.Sprintf("%s (%s)", a, aType)
|
||
|
b = fmt.Sprintf("%s (%s)", b, bType)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
switch {
|
||
|
case len(a) > max && len(a) > 4 && len(b) > 4 && a[:4] == b[:4]:
|
||
|
// a is too long, b has data, and the first several characters are the same
|
||
|
elidedPrefix = "..."
|
||
|
a = a[2:]
|
||
|
b = b[2:]
|
||
|
|
||
|
case len(b) > max && len(b) > 4 && len(a) > 4 && a[:4] == b[:4]:
|
||
|
// b is too long, a has data, and the first several characters are the same
|
||
|
elidedPrefix = "..."
|
||
|
a = a[2:]
|
||
|
b = b[2:]
|
||
|
|
||
|
case len(a) > max:
|
||
|
a = a[:max]
|
||
|
elidedASuffix = "..."
|
||
|
|
||
|
case len(b) > max:
|
||
|
b = b[:max]
|
||
|
elidedBSuffix = "..."
|
||
|
|
||
|
default:
|
||
|
// both are short enough
|
||
|
return elidedPrefix + a + elidedASuffix, elidedPrefix + b + elidedBSuffix
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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()) {
|
||
|
continue
|
||
|
}
|
||
|
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, reflect.Interface:
|
||
|
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:
|
||
|
lA, lB := a.Len(), b.Len()
|
||
|
l := lA
|
||
|
if lB < lA {
|
||
|
l = lB
|
||
|
}
|
||
|
if lA == lB && lA == 0 {
|
||
|
if a.IsNil() != b.IsNil() {
|
||
|
return []diff{{path: path, a: a.Interface(), b: b.Interface()}}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
var diffs []diff
|
||
|
for i := 0; i < l; i++ {
|
||
|
if !reflect.DeepEqual(a.Index(i), b.Index(i)) {
|
||
|
diffs = append(diffs, objectReflectDiff(path.Index(i), a.Index(i), b.Index(i))...)
|
||
|
}
|
||
|
}
|
||
|
for i := l; i < lA; i++ {
|
||
|
diffs = append(diffs, diff{path: path.Index(i), a: a.Index(i), b: nil})
|
||
|
}
|
||
|
for i := l; i < 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.Interface(), b.Interface()) {
|
||
|
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, objectReflectDiff(path.Key(fmt.Sprintf("%s", key.Interface())), a.MapIndex(key), b.MapIndex(key))...)
|
||
|
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})
|
||
|
}
|
||
|
if len(missing) == 0 {
|
||
|
missing = append(missing, diff{path: path, a: a.Interface(), b: b.Interface()})
|
||
|
}
|
||
|
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,
|
||
|
// enabling easy visual scanning for mismatches.
|
||
|
func ObjectGoPrintSideBySide(a, b interface{}) string {
|
||
|
s := spew.ConfigState{
|
||
|
Indent: " ",
|
||
|
// Extra deep spew.
|
||
|
DisableMethods: true,
|
||
|
}
|
||
|
sA := s.Sdump(a)
|
||
|
sB := s.Sdump(b)
|
||
|
|
||
|
linesA := strings.Split(sA, "\n")
|
||
|
linesB := strings.Split(sB, "\n")
|
||
|
width := 0
|
||
|
for _, s := range linesA {
|
||
|
l := len(s)
|
||
|
if l > width {
|
||
|
width = l
|
||
|
}
|
||
|
}
|
||
|
for _, s := range linesB {
|
||
|
l := len(s)
|
||
|
if l > width {
|
||
|
width = l
|
||
|
}
|
||
|
}
|
||
|
buf := &bytes.Buffer{}
|
||
|
w := tabwriter.NewWriter(buf, width, 0, 1, ' ', 0)
|
||
|
max := len(linesA)
|
||
|
if len(linesB) > max {
|
||
|
max = len(linesB)
|
||
|
}
|
||
|
for i := 0; i < max; i++ {
|
||
|
var a, b string
|
||
|
if i < len(linesA) {
|
||
|
a = linesA[i]
|
||
|
}
|
||
|
if i < len(linesB) {
|
||
|
b = linesB[i]
|
||
|
}
|
||
|
fmt.Fprintf(w, "%s\t%s\n", a, b)
|
||
|
}
|
||
|
w.Flush()
|
||
|
return buf.String()
|
||
|
}
|