243 lines
5.9 KiB
Go
243 lines
5.9 KiB
Go
|
package messagediff
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"unsafe"
|
||
|
)
|
||
|
|
||
|
// PrettyDiff does a deep comparison and returns the nicely formated results.
|
||
|
func PrettyDiff(a, b interface{}) (string, bool) {
|
||
|
d, equal := DeepDiff(a, b)
|
||
|
var dstr []string
|
||
|
for path, added := range d.Added {
|
||
|
dstr = append(dstr, fmt.Sprintf("added: %s = %#v\n", path.String(), added))
|
||
|
}
|
||
|
for path, removed := range d.Removed {
|
||
|
dstr = append(dstr, fmt.Sprintf("removed: %s = %#v\n", path.String(), removed))
|
||
|
}
|
||
|
for path, modified := range d.Modified {
|
||
|
dstr = append(dstr, fmt.Sprintf("modified: %s = %#v\n", path.String(), modified))
|
||
|
}
|
||
|
sort.Strings(dstr)
|
||
|
return strings.Join(dstr, ""), equal
|
||
|
}
|
||
|
|
||
|
// DeepDiff does a deep comparison and returns the results.
|
||
|
func DeepDiff(a, b interface{}) (*Diff, bool) {
|
||
|
d := newDiff()
|
||
|
return d, d.diff(reflect.ValueOf(a), reflect.ValueOf(b), nil)
|
||
|
}
|
||
|
|
||
|
func newDiff() *Diff {
|
||
|
return &Diff{
|
||
|
Added: make(map[*Path]interface{}),
|
||
|
Removed: make(map[*Path]interface{}),
|
||
|
Modified: make(map[*Path]interface{}),
|
||
|
visited: make(map[visit]bool),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d *Diff) diff(aVal, bVal reflect.Value, path Path) bool {
|
||
|
// The array underlying `path` could be modified in subsequent
|
||
|
// calls. Make sure we have a local copy.
|
||
|
localPath := make(Path, len(path))
|
||
|
copy(localPath, path)
|
||
|
|
||
|
// Validity checks. Should only trigger if nil is one of the original arguments.
|
||
|
if !aVal.IsValid() && !bVal.IsValid() {
|
||
|
return true
|
||
|
}
|
||
|
if !bVal.IsValid() {
|
||
|
d.Modified[&localPath] = nil
|
||
|
return false
|
||
|
} else if !aVal.IsValid() {
|
||
|
d.Modified[&localPath] = bVal.Interface()
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if aVal.Type() != bVal.Type() {
|
||
|
d.Modified[&localPath] = bVal.Interface()
|
||
|
return false
|
||
|
}
|
||
|
kind := aVal.Kind()
|
||
|
|
||
|
// Borrowed from the reflect package to handle recursive data structures.
|
||
|
hard := func(k reflect.Kind) bool {
|
||
|
switch k {
|
||
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if aVal.CanAddr() && bVal.CanAddr() && hard(kind) {
|
||
|
addr1 := unsafe.Pointer(aVal.UnsafeAddr())
|
||
|
addr2 := unsafe.Pointer(bVal.UnsafeAddr())
|
||
|
if uintptr(addr1) > uintptr(addr2) {
|
||
|
// Canonicalize order to reduce number of entries in visited.
|
||
|
// Assumes non-moving garbage collector.
|
||
|
addr1, addr2 = addr2, addr1
|
||
|
}
|
||
|
|
||
|
// Short circuit if references are already seen.
|
||
|
typ := aVal.Type()
|
||
|
v := visit{addr1, addr2, typ}
|
||
|
if d.visited[v] {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// Remember for later.
|
||
|
d.visited[v] = true
|
||
|
}
|
||
|
// End of borrowed code.
|
||
|
|
||
|
equal := true
|
||
|
switch kind {
|
||
|
case reflect.Map, reflect.Ptr, reflect.Func, reflect.Chan, reflect.Slice:
|
||
|
if aVal.IsNil() && bVal.IsNil() {
|
||
|
return true
|
||
|
}
|
||
|
if aVal.IsNil() || bVal.IsNil() {
|
||
|
d.Modified[&localPath] = bVal.Interface()
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch kind {
|
||
|
case reflect.Array, reflect.Slice:
|
||
|
aLen := aVal.Len()
|
||
|
bLen := bVal.Len()
|
||
|
for i := 0; i < min(aLen, bLen); i++ {
|
||
|
localPath := append(localPath, SliceIndex(i))
|
||
|
if eq := d.diff(aVal.Index(i), bVal.Index(i), localPath); !eq {
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
if aLen > bLen {
|
||
|
for i := bLen; i < aLen; i++ {
|
||
|
localPath := append(localPath, SliceIndex(i))
|
||
|
d.Removed[&localPath] = aVal.Index(i).Interface()
|
||
|
equal = false
|
||
|
}
|
||
|
} else if aLen < bLen {
|
||
|
for i := aLen; i < bLen; i++ {
|
||
|
localPath := append(localPath, SliceIndex(i))
|
||
|
d.Added[&localPath] = bVal.Index(i).Interface()
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
case reflect.Map:
|
||
|
for _, key := range aVal.MapKeys() {
|
||
|
aI := aVal.MapIndex(key)
|
||
|
bI := bVal.MapIndex(key)
|
||
|
localPath := append(localPath, MapKey{key.Interface()})
|
||
|
if !bI.IsValid() {
|
||
|
d.Removed[&localPath] = aI.Interface()
|
||
|
equal = false
|
||
|
} else if eq := d.diff(aI, bI, localPath); !eq {
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
for _, key := range bVal.MapKeys() {
|
||
|
aI := aVal.MapIndex(key)
|
||
|
if !aI.IsValid() {
|
||
|
bI := bVal.MapIndex(key)
|
||
|
localPath := append(localPath, MapKey{key.Interface()})
|
||
|
d.Added[&localPath] = bI.Interface()
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
case reflect.Struct:
|
||
|
typ := aVal.Type()
|
||
|
for i := 0; i < typ.NumField(); i++ {
|
||
|
index := []int{i}
|
||
|
field := typ.FieldByIndex(index)
|
||
|
if field.Tag.Get("testdiff") == "ignore" { // skip fields marked to be ignored
|
||
|
continue
|
||
|
}
|
||
|
localPath := append(localPath, StructField(field.Name))
|
||
|
aI := unsafeReflectValue(aVal.FieldByIndex(index))
|
||
|
bI := unsafeReflectValue(bVal.FieldByIndex(index))
|
||
|
if eq := d.diff(aI, bI, localPath); !eq {
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
case reflect.Ptr:
|
||
|
equal = d.diff(aVal.Elem(), bVal.Elem(), localPath)
|
||
|
default:
|
||
|
if reflect.DeepEqual(aVal.Interface(), bVal.Interface()) {
|
||
|
equal = true
|
||
|
} else {
|
||
|
d.Modified[&localPath] = bVal.Interface()
|
||
|
equal = false
|
||
|
}
|
||
|
}
|
||
|
return equal
|
||
|
}
|
||
|
|
||
|
func min(a, b int) int {
|
||
|
if a < b {
|
||
|
return a
|
||
|
}
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// During deepValueEqual, must keep track of checks that are
|
||
|
// in progress. The comparison algorithm assumes that all
|
||
|
// checks in progress are true when it reencounters them.
|
||
|
// Visited comparisons are stored in a map indexed by visit.
|
||
|
// This is borrowed from the reflect package.
|
||
|
type visit struct {
|
||
|
a1 unsafe.Pointer
|
||
|
a2 unsafe.Pointer
|
||
|
typ reflect.Type
|
||
|
}
|
||
|
|
||
|
// Diff represents a change in a struct.
|
||
|
type Diff struct {
|
||
|
Added, Removed, Modified map[*Path]interface{}
|
||
|
visited map[visit]bool
|
||
|
}
|
||
|
|
||
|
// Path represents a path to a changed datum.
|
||
|
type Path []PathNode
|
||
|
|
||
|
func (p Path) String() string {
|
||
|
var out string
|
||
|
for _, n := range p {
|
||
|
out += n.String()
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// PathNode represents one step in the path.
|
||
|
type PathNode interface {
|
||
|
String() string
|
||
|
}
|
||
|
|
||
|
// StructField is a path element representing a field of a struct.
|
||
|
type StructField string
|
||
|
|
||
|
func (n StructField) String() string {
|
||
|
return fmt.Sprintf(".%s", string(n))
|
||
|
}
|
||
|
|
||
|
// MapKey is a path element representing a key of a map.
|
||
|
type MapKey struct {
|
||
|
Key interface{}
|
||
|
}
|
||
|
|
||
|
func (n MapKey) String() string {
|
||
|
return fmt.Sprintf("[%#v]", n.Key)
|
||
|
}
|
||
|
|
||
|
// SliceIndex is a path element representing a index of a slice.
|
||
|
type SliceIndex int
|
||
|
|
||
|
func (n SliceIndex) String() string {
|
||
|
return fmt.Sprintf("[%d]", n)
|
||
|
}
|