435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
334 lines
10 KiB
Go
334 lines
10 KiB
Go
// Copyright 2019, The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE.md file.
|
|
|
|
package cmp
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/google/go-cmp/cmp/internal/diff"
|
|
)
|
|
|
|
// CanFormatDiffSlice reports whether we support custom formatting for nodes
|
|
// that are slices of primitive kinds or strings.
|
|
func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
|
|
switch {
|
|
case opts.DiffMode != diffUnknown:
|
|
return false // Must be formatting in diff mode
|
|
case v.NumDiff == 0:
|
|
return false // No differences detected
|
|
case v.NumIgnored+v.NumCompared+v.NumTransformed > 0:
|
|
// TODO: Handle the case where someone uses bytes.Equal on a large slice.
|
|
return false // Some custom option was used to determined equality
|
|
case !v.ValueX.IsValid() || !v.ValueY.IsValid():
|
|
return false // Both values must be valid
|
|
}
|
|
|
|
switch t := v.Type; t.Kind() {
|
|
case reflect.String:
|
|
case reflect.Array, reflect.Slice:
|
|
// Only slices of primitive types have specialized handling.
|
|
switch t.Elem().Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
|
reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
|
|
default:
|
|
return false
|
|
}
|
|
|
|
// If a sufficient number of elements already differ,
|
|
// use specialized formatting even if length requirement is not met.
|
|
if v.NumDiff > v.NumSame {
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
|
|
// Use specialized string diffing for longer slices or strings.
|
|
const minLength = 64
|
|
return v.ValueX.Len() >= minLength && v.ValueY.Len() >= minLength
|
|
}
|
|
|
|
// FormatDiffSlice prints a diff for the slices (or strings) represented by v.
|
|
// This provides custom-tailored logic to make printing of differences in
|
|
// textual strings and slices of primitive kinds more readable.
|
|
func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
|
|
assert(opts.DiffMode == diffUnknown)
|
|
t, vx, vy := v.Type, v.ValueX, v.ValueY
|
|
|
|
// Auto-detect the type of the data.
|
|
var isLinedText, isText, isBinary bool
|
|
var sx, sy string
|
|
switch {
|
|
case t.Kind() == reflect.String:
|
|
sx, sy = vx.String(), vy.String()
|
|
isText = true // Initial estimate, verify later
|
|
case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)):
|
|
sx, sy = string(vx.Bytes()), string(vy.Bytes())
|
|
isBinary = true // Initial estimate, verify later
|
|
case t.Kind() == reflect.Array:
|
|
// Arrays need to be addressable for slice operations to work.
|
|
vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem()
|
|
vx2.Set(vx)
|
|
vy2.Set(vy)
|
|
vx, vy = vx2, vy2
|
|
}
|
|
if isText || isBinary {
|
|
var numLines, lastLineIdx, maxLineLen int
|
|
isBinary = false
|
|
for i, r := range sx + sy {
|
|
if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
|
|
isBinary = true
|
|
break
|
|
}
|
|
if r == '\n' {
|
|
if maxLineLen < i-lastLineIdx {
|
|
maxLineLen = i - lastLineIdx
|
|
}
|
|
lastLineIdx = i + 1
|
|
numLines++
|
|
}
|
|
}
|
|
isText = !isBinary
|
|
isLinedText = isText && numLines >= 4 && maxLineLen <= 256
|
|
}
|
|
|
|
// Format the string into printable records.
|
|
var list textList
|
|
var delim string
|
|
switch {
|
|
// If the text appears to be multi-lined text,
|
|
// then perform differencing across individual lines.
|
|
case isLinedText:
|
|
ssx := strings.Split(sx, "\n")
|
|
ssy := strings.Split(sy, "\n")
|
|
list = opts.formatDiffSlice(
|
|
reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line",
|
|
func(v reflect.Value, d diffMode) textRecord {
|
|
s := formatString(v.Index(0).String())
|
|
return textRecord{Diff: d, Value: textLine(s)}
|
|
},
|
|
)
|
|
delim = "\n"
|
|
// If the text appears to be single-lined text,
|
|
// then perform differencing in approximately fixed-sized chunks.
|
|
// The output is printed as quoted strings.
|
|
case isText:
|
|
list = opts.formatDiffSlice(
|
|
reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte",
|
|
func(v reflect.Value, d diffMode) textRecord {
|
|
s := formatString(v.String())
|
|
return textRecord{Diff: d, Value: textLine(s)}
|
|
},
|
|
)
|
|
delim = ""
|
|
// If the text appears to be binary data,
|
|
// then perform differencing in approximately fixed-sized chunks.
|
|
// The output is inspired by hexdump.
|
|
case isBinary:
|
|
list = opts.formatDiffSlice(
|
|
reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte",
|
|
func(v reflect.Value, d diffMode) textRecord {
|
|
var ss []string
|
|
for i := 0; i < v.Len(); i++ {
|
|
ss = append(ss, formatHex(v.Index(i).Uint()))
|
|
}
|
|
s := strings.Join(ss, ", ")
|
|
comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String())))
|
|
return textRecord{Diff: d, Value: textLine(s), Comment: comment}
|
|
},
|
|
)
|
|
// For all other slices of primitive types,
|
|
// then perform differencing in approximately fixed-sized chunks.
|
|
// The size of each chunk depends on the width of the element kind.
|
|
default:
|
|
var chunkSize int
|
|
if t.Elem().Kind() == reflect.Bool {
|
|
chunkSize = 16
|
|
} else {
|
|
switch t.Elem().Bits() {
|
|
case 8:
|
|
chunkSize = 16
|
|
case 16:
|
|
chunkSize = 12
|
|
case 32:
|
|
chunkSize = 8
|
|
default:
|
|
chunkSize = 8
|
|
}
|
|
}
|
|
list = opts.formatDiffSlice(
|
|
vx, vy, chunkSize, t.Elem().Kind().String(),
|
|
func(v reflect.Value, d diffMode) textRecord {
|
|
var ss []string
|
|
for i := 0; i < v.Len(); i++ {
|
|
switch t.Elem().Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
ss = append(ss, fmt.Sprint(v.Index(i).Int()))
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
ss = append(ss, formatHex(v.Index(i).Uint()))
|
|
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
|
|
ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
|
|
}
|
|
}
|
|
s := strings.Join(ss, ", ")
|
|
return textRecord{Diff: d, Value: textLine(s)}
|
|
},
|
|
)
|
|
}
|
|
|
|
// Wrap the output with appropriate type information.
|
|
var out textNode = textWrap{"{", list, "}"}
|
|
if !isText {
|
|
// The "{...}" byte-sequence literal is not valid Go syntax for strings.
|
|
// Emit the type for extra clarity (e.g. "string{...}").
|
|
if t.Kind() == reflect.String {
|
|
opts = opts.WithTypeMode(emitType)
|
|
}
|
|
return opts.FormatType(t, out)
|
|
}
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)}
|
|
if t != reflect.TypeOf(string("")) {
|
|
out = opts.FormatType(t, out)
|
|
}
|
|
case reflect.Slice:
|
|
out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)}
|
|
if t != reflect.TypeOf([]byte(nil)) {
|
|
out = opts.FormatType(t, out)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// formatASCII formats s as an ASCII string.
|
|
// This is useful for printing binary strings in a semi-legible way.
|
|
func formatASCII(s string) string {
|
|
b := bytes.Repeat([]byte{'.'}, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
if ' ' <= s[i] && s[i] <= '~' {
|
|
b[i] = s[i]
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func (opts formatOptions) formatDiffSlice(
|
|
vx, vy reflect.Value, chunkSize int, name string,
|
|
makeRec func(reflect.Value, diffMode) textRecord,
|
|
) (list textList) {
|
|
es := diff.Difference(vx.Len(), vy.Len(), func(ix int, iy int) diff.Result {
|
|
return diff.BoolResult(vx.Index(ix).Interface() == vy.Index(iy).Interface())
|
|
})
|
|
|
|
appendChunks := func(v reflect.Value, d diffMode) int {
|
|
n0 := v.Len()
|
|
for v.Len() > 0 {
|
|
n := chunkSize
|
|
if n > v.Len() {
|
|
n = v.Len()
|
|
}
|
|
list = append(list, makeRec(v.Slice(0, n), d))
|
|
v = v.Slice(n, v.Len())
|
|
}
|
|
return n0 - v.Len()
|
|
}
|
|
|
|
groups := coalesceAdjacentEdits(name, es)
|
|
groups = coalesceInterveningIdentical(groups, chunkSize/4)
|
|
for i, ds := range groups {
|
|
// Print equal.
|
|
if ds.NumDiff() == 0 {
|
|
// Compute the number of leading and trailing equal bytes to print.
|
|
var numLo, numHi int
|
|
numEqual := ds.NumIgnored + ds.NumIdentical
|
|
for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 {
|
|
numLo++
|
|
}
|
|
for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
|
|
numHi++
|
|
}
|
|
if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 {
|
|
numHi = numEqual - numLo // Avoid pointless coalescing of single equal row
|
|
}
|
|
|
|
// Print the equal bytes.
|
|
appendChunks(vx.Slice(0, numLo), diffIdentical)
|
|
if numEqual > numLo+numHi {
|
|
ds.NumIdentical -= numLo + numHi
|
|
list.AppendEllipsis(ds)
|
|
}
|
|
appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical)
|
|
vx = vx.Slice(numEqual, vx.Len())
|
|
vy = vy.Slice(numEqual, vy.Len())
|
|
continue
|
|
}
|
|
|
|
// Print unequal.
|
|
nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
|
|
vx = vx.Slice(nx, vx.Len())
|
|
ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
|
|
vy = vy.Slice(ny, vy.Len())
|
|
}
|
|
assert(vx.Len() == 0 && vy.Len() == 0)
|
|
return list
|
|
}
|
|
|
|
// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent
|
|
// equal or unequal counts.
|
|
func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) {
|
|
var prevCase int // Arbitrary index into which case last occurred
|
|
lastStats := func(i int) *diffStats {
|
|
if prevCase != i {
|
|
groups = append(groups, diffStats{Name: name})
|
|
prevCase = i
|
|
}
|
|
return &groups[len(groups)-1]
|
|
}
|
|
for _, e := range es {
|
|
switch e {
|
|
case diff.Identity:
|
|
lastStats(1).NumIdentical++
|
|
case diff.UniqueX:
|
|
lastStats(2).NumRemoved++
|
|
case diff.UniqueY:
|
|
lastStats(2).NumInserted++
|
|
case diff.Modified:
|
|
lastStats(2).NumModified++
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// coalesceInterveningIdentical coalesces sufficiently short (<= windowSize)
|
|
// equal groups into adjacent unequal groups that currently result in a
|
|
// dual inserted/removed printout. This acts as a high-pass filter to smooth
|
|
// out high-frequency changes within the windowSize.
|
|
func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats {
|
|
groups, groupsOrig := groups[:0], groups
|
|
for i, ds := range groupsOrig {
|
|
if len(groups) >= 2 && ds.NumDiff() > 0 {
|
|
prev := &groups[len(groups)-2] // Unequal group
|
|
curr := &groups[len(groups)-1] // Equal group
|
|
next := &groupsOrig[i] // Unequal group
|
|
hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0
|
|
hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0
|
|
if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize {
|
|
*prev = prev.Append(*curr).Append(*next)
|
|
groups = groups[:len(groups)-1] // Truncate off equal group
|
|
continue
|
|
}
|
|
}
|
|
groups = append(groups, ds)
|
|
}
|
|
return groups
|
|
}
|