bump gotest.tools v2.3.0, google/go-cmp v0.2.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-04-05 10:52:24 +02:00
parent 591e52c504
commit 1978c0b74b
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
23 changed files with 399 additions and 424 deletions

View File

@ -39,8 +39,8 @@ google.golang.org/genproto d80a6e20e776b0b17a324d0ba1ab50a39c8e8944
golang.org/x/text 19e51611da83d6be54ddafce4a4af510cb3e9ea4 golang.org/x/text 19e51611da83d6be54ddafce4a4af510cb3e9ea4
github.com/containerd/ttrpc f02858b1457c5ca3aaec3a0803eb0d59f96e41d6 github.com/containerd/ttrpc f02858b1457c5ca3aaec3a0803eb0d59f96e41d6
github.com/syndtr/gocapability db04d3cc01c8b54962a58ec7e491717d06cfcc16 github.com/syndtr/gocapability db04d3cc01c8b54962a58ec7e491717d06cfcc16
gotest.tools v2.1.0 gotest.tools v2.3.0
github.com/google/go-cmp v0.1.0 github.com/google/go-cmp v0.2.0
go.etcd.io/bbolt v1.3.2 go.etcd.io/bbolt v1.3.2
# cri dependencies # cri dependencies

View File

@ -21,7 +21,7 @@ The primary features of `cmp` are:
equality is determined by recursively comparing the primitive kinds on both equality is determined by recursively comparing the primitive kinds on both
values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported
fields are not compared by default; they result in panics unless suppressed fields are not compared by default; they result in panics unless suppressed
by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explictly by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly
compared using the `AllowUnexported` option. compared using the `AllowUnexported` option.
See the [GoDoc documentation][godoc] for more information. See the [GoDoc documentation][godoc] for more information.

View File

@ -17,7 +17,7 @@ func equateAlways(_, _ interface{}) bool { return true }
// EquateEmpty returns a Comparer option that determines all maps and slices // EquateEmpty returns a Comparer option that determines all maps and slices
// with a length of zero to be equal, regardless of whether they are nil. // with a length of zero to be equal, regardless of whether they are nil.
// //
// EquateEmpty can be used in conjuction with SortSlices and SortMaps. // EquateEmpty can be used in conjunction with SortSlices and SortMaps.
func EquateEmpty() cmp.Option { func EquateEmpty() cmp.Option {
return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways))
} }
@ -42,7 +42,7 @@ func isEmpty(x, y interface{}) bool {
// The mathematical expression used is equivalent to: // The mathematical expression used is equivalent to:
// |x-y| ≤ max(fraction*min(|x|, |y|), margin) // |x-y| ≤ max(fraction*min(|x|, |y|), margin)
// //
// EquateApprox can be used in conjuction with EquateNaNs. // EquateApprox can be used in conjunction with EquateNaNs.
func EquateApprox(fraction, margin float64) cmp.Option { func EquateApprox(fraction, margin float64) cmp.Option {
if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) {
panic("margin or fraction must be a non-negative number") panic("margin or fraction must be a non-negative number")
@ -73,7 +73,7 @@ func (a approximator) compareF32(x, y float32) bool {
// EquateNaNs returns a Comparer option that determines float32 and float64 // EquateNaNs returns a Comparer option that determines float32 and float64
// NaN values to be equal. // NaN values to be equal.
// //
// EquateNaNs can be used in conjuction with EquateApprox. // EquateNaNs can be used in conjunction with EquateApprox.
func EquateNaNs() cmp.Option { func EquateNaNs() cmp.Option {
return cmp.Options{ return cmp.Options{
cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)),

View File

@ -50,7 +50,7 @@ func (tf typeFilter) filter(p cmp.Path) bool {
if len(p) < 1 { if len(p) < 1 {
return false return false
} }
t := p[len(p)-1].Type() t := p.Last().Type()
for _, ti := range tf { for _, ti := range tf {
if t.AssignableTo(ti) { if t.AssignableTo(ti) {
return true return true
@ -95,7 +95,7 @@ func (tf ifaceFilter) filter(p cmp.Path) bool {
if len(p) < 1 { if len(p) < 1 {
return false return false
} }
t := p[len(p)-1].Type() t := p.Last().Type()
for _, ti := range tf { for _, ti := range tf {
if t.AssignableTo(ti) { if t.AssignableTo(ti) {
return true return true
@ -131,14 +131,11 @@ func newUnexportedFilter(typs ...interface{}) unexportedFilter {
return ux return ux
} }
func (xf unexportedFilter) filter(p cmp.Path) bool { func (xf unexportedFilter) filter(p cmp.Path) bool {
if len(p) < 2 { sf, ok := p.Index(-1).(cmp.StructField)
return false
}
sf, ok := p[len(p)-1].(cmp.StructField)
if !ok { if !ok {
return false return false
} }
return xf.m[p[len(p)-2].Type()] && !isExported(sf.Name()) return xf.m[p.Index(-2).Type()] && !isExported(sf.Name())
} }
// isExported reports whether the identifier is exported. // isExported reports whether the identifier is exported.

View File

@ -24,7 +24,7 @@ import (
// The less function does not have to be "total". That is, if !less(x, y) and // The less function does not have to be "total". That is, if !less(x, y) and
// !less(y, x) for two elements x and y, their relative order is maintained. // !less(y, x) for two elements x and y, their relative order is maintained.
// //
// SortSlices can be used in conjuction with EquateEmpty. // SortSlices can be used in conjunction with EquateEmpty.
func SortSlices(less interface{}) cmp.Option { func SortSlices(less interface{}) cmp.Option {
vf := reflect.ValueOf(less) vf := reflect.ValueOf(less)
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
@ -95,7 +95,7 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) // • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
// • Total: if x != y, then either less(x, y) or less(y, x) // • Total: if x != y, then either less(x, y) or less(y, x)
// //
// SortMaps can be used in conjuction with EquateEmpty. // SortMaps can be used in conjunction with EquateEmpty.
func SortMaps(less interface{}) cmp.Option { func SortMaps(less interface{}) cmp.Option {
vf := reflect.ValueOf(less) vf := reflect.ValueOf(less)
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {

View File

@ -22,7 +22,7 @@
// equality is determined by recursively comparing the primitive kinds on both // equality is determined by recursively comparing the primitive kinds on both
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported // values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
// fields are not compared by default; they result in panics unless suppressed // fields are not compared by default; they result in panics unless suppressed
// by using an Ignore option (see cmpopts.IgnoreUnexported) or explictly compared // by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared
// using the AllowUnexported option. // using the AllowUnexported option.
package cmp package cmp
@ -35,7 +35,7 @@ import (
"github.com/google/go-cmp/cmp/internal/value" "github.com/google/go-cmp/cmp/internal/value"
) )
// BUG: Maps with keys containing NaN values cannot be properly compared due to // BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to
// the reflection package's inability to retrieve such entries. Equal will panic // the reflection package's inability to retrieve such entries. Equal will panic
// anytime it comes across a NaN key, but this behavior may change. // anytime it comes across a NaN key, but this behavior may change.
// //
@ -61,8 +61,8 @@ var nothing = reflect.Value{}
// //
// • If the values have an Equal method of the form "(T) Equal(T) bool" or // • If the values have an Equal method of the form "(T) Equal(T) bool" or
// "(T) Equal(I) bool" where T is assignable to I, then use the result of // "(T) Equal(I) bool" where T is assignable to I, then use the result of
// x.Equal(y). Otherwise, no such method exists and evaluation proceeds to // x.Equal(y) even if x or y is nil.
// the next rule. // Otherwise, no such method exists and evaluation proceeds to the next rule.
// //
// • Lastly, try to compare x and y based on their basic kinds. // • Lastly, try to compare x and y based on their basic kinds.
// Simple kinds like booleans, integers, floats, complex numbers, strings, and // Simple kinds like booleans, integers, floats, complex numbers, strings, and
@ -304,7 +304,8 @@ func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
// Evaluate all filters and apply the remaining options. // Evaluate all filters and apply the remaining options.
if opt := opts.filter(s, vx, vy, t); opt != nil { if opt := opts.filter(s, vx, vy, t); opt != nil {
return opt.apply(s, vx, vy) opt.apply(s, vx, vy)
return true
} }
return false return false
} }
@ -322,6 +323,7 @@ func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {
} }
func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
v = sanitizeValue(v, f.Type().In(0))
if !s.dynChecker.Next() { if !s.dynChecker.Next() {
return f.Call([]reflect.Value{v})[0] return f.Call([]reflect.Value{v})[0]
} }
@ -345,6 +347,8 @@ func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
} }
func (s *state) callTTBFunc(f, x, y reflect.Value) bool { func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
x = sanitizeValue(x, f.Type().In(0))
y = sanitizeValue(y, f.Type().In(1))
if !s.dynChecker.Next() { if !s.dynChecker.Next() {
return f.Call([]reflect.Value{x, y})[0].Bool() return f.Call([]reflect.Value{x, y})[0].Bool()
} }
@ -372,20 +376,40 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
ret = f.Call(vs)[0] ret = f.Call(vs)[0]
} }
// sanitizeValue converts nil interfaces of type T to those of type R,
// assuming that T is assignable to R.
// Otherwise, it returns the input value as is.
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
// TODO(dsnet): Remove this hacky workaround.
// See https://golang.org/issue/22143
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
return reflect.New(t).Elem()
}
return v
}
func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) { func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) {
step := &sliceIndex{pathStep{t.Elem()}, 0, 0} step := &sliceIndex{pathStep{t.Elem()}, 0, 0}
s.curPath.push(step) s.curPath.push(step)
// Compute an edit-script for slices vx and vy. // Compute an edit-script for slices vx and vy.
eq, es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
step.xkey, step.ykey = ix, iy step.xkey, step.ykey = ix, iy
return s.statelessCompare(vx.Index(ix), vy.Index(iy)) return s.statelessCompare(vx.Index(ix), vy.Index(iy))
}) })
// Equal or no edit-script, so report entire slices as is. // Report the entire slice as is if the arrays are of primitive kind,
if eq || es == nil { // and the arrays are different enough.
isPrimitive := false
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:
isPrimitive = true
}
if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 {
s.curPath.pop() // Pop first since we are reporting the whole slice s.curPath.pop() // Pop first since we are reporting the whole slice
s.report(eq, vx, vy) s.report(false, vx, vy)
return return
} }

View File

@ -50,7 +50,7 @@ import (
// //
// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents // The series of '.', 'X', 'Y', and 'M' characters at the bottom represents
// the currently established path from the forward and reverse searches, // the currently established path from the forward and reverse searches,
// seperated by a '|' character. // separated by a '|' character.
const ( const (
updateDelay = 100 * time.Millisecond updateDelay = 100 * time.Millisecond

View File

@ -106,9 +106,9 @@ func (r Result) Similar() bool {
// Difference reports whether two lists of lengths nx and ny are equal // Difference reports whether two lists of lengths nx and ny are equal
// given the definition of equality provided as f. // given the definition of equality provided as f.
// //
// This function may return a edit-script, which is a sequence of operations // This function returns an edit-script, which is a sequence of operations
// needed to convert one list into the other. If non-nil, the following // needed to convert one list into the other. The following invariants for
// invariants for the edit-script are maintained: // the edit-script are maintained:
// • eq == (es.Dist()==0) // • eq == (es.Dist()==0)
// • nx == es.LenX() // • nx == es.LenX()
// • ny == es.LenY() // • ny == es.LenY()
@ -117,17 +117,7 @@ func (r Result) Similar() bool {
// produces an edit-script with a minimal Levenshtein distance). This algorithm // produces an edit-script with a minimal Levenshtein distance). This algorithm
// favors performance over optimality. The exact output is not guaranteed to // favors performance over optimality. The exact output is not guaranteed to
// be stable and may change over time. // be stable and may change over time.
func Difference(nx, ny int, f EqualFunc) (eq bool, es EditScript) { func Difference(nx, ny int, f EqualFunc) (es EditScript) {
es = searchGraph(nx, ny, f)
st := es.stats()
eq = len(es) == st.NI
if !eq && st.NI < (nx+ny)/4 {
return eq, nil // Edit-script more distracting than helpful
}
return eq, es
}
func searchGraph(nx, ny int, f EqualFunc) EditScript {
// This algorithm is based on traversing what is known as an "edit-graph". // This algorithm is based on traversing what is known as an "edit-graph".
// See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations"
// by Eugene W. Myers. Since D can be as large as N itself, this is // by Eugene W. Myers. Since D can be as large as N itself, this is

View File

@ -8,15 +8,11 @@ package value
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strconv"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
) )
// formatFakePointers controls whether to substitute pointer addresses with nil.
// This is used for deterministic testing.
var formatFakePointers = false
var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
// Format formats the value v as a string. // Format formats the value v as a string.
@ -26,28 +22,35 @@ var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
// * Avoids printing struct fields that are zero // * Avoids printing struct fields that are zero
// * Prints a nil-slice as being nil, not empty // * Prints a nil-slice as being nil, not empty
// * Prints map entries in deterministic order // * Prints map entries in deterministic order
func Format(v reflect.Value, useStringer bool) string { func Format(v reflect.Value, conf FormatConfig) string {
return formatAny(v, formatConfig{useStringer, true, true, !formatFakePointers}, nil) conf.printType = true
conf.followPointers = true
conf.realPointers = true
return formatAny(v, conf, nil)
} }
type formatConfig struct { type FormatConfig struct {
useStringer bool // Should the String method be used if available? UseStringer bool // Should the String method be used if available?
printType bool // Should we print the type before the value? printType bool // Should we print the type before the value?
PrintPrimitiveType bool // Should we print the type of primitives?
followPointers bool // Should we recursively follow pointers? followPointers bool // Should we recursively follow pointers?
realPointers bool // Should we print the real address of pointers? realPointers bool // Should we print the real address of pointers?
} }
func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) string { func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string {
// TODO: Should this be a multi-line printout in certain situations? // TODO: Should this be a multi-line printout in certain situations?
if !v.IsValid() { if !v.IsValid() {
return "<non-existent>" return "<non-existent>"
} }
if conf.useStringer && v.Type().Implements(stringerIface) && v.CanInterface() { if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
return "<nil>" return "<nil>"
} }
return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String())
const stringerPrefix = "s" // Indicates that the String method was used
s := v.Interface().(fmt.Stringer).String()
return stringerPrefix + formatString(s)
} }
switch v.Kind() { switch v.Kind() {
@ -66,7 +69,7 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str
case reflect.Complex64, reflect.Complex128: case reflect.Complex64, reflect.Complex128:
return formatPrimitive(v.Type(), v.Complex(), conf) return formatPrimitive(v.Type(), v.Complex(), conf)
case reflect.String: case reflect.String:
return formatPrimitive(v.Type(), fmt.Sprintf("%q", v), conf) return formatPrimitive(v.Type(), formatString(v.String()), conf)
case reflect.UnsafePointer, reflect.Chan, reflect.Func: case reflect.UnsafePointer, reflect.Chan, reflect.Func:
return formatPointer(v, conf) return formatPointer(v, conf)
case reflect.Ptr: case reflect.Ptr:
@ -127,11 +130,13 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str
visited = insertPointer(visited, v.Pointer()) visited = insertPointer(visited, v.Pointer())
var ss []string var ss []string
subConf := conf keyConf, valConf := conf, conf
subConf.printType = v.Type().Elem().Kind() == reflect.Interface keyConf.printType = v.Type().Key().Kind() == reflect.Interface
keyConf.followPointers = false
valConf.printType = v.Type().Elem().Kind() == reflect.Interface
for _, k := range SortKeys(v.MapKeys()) { for _, k := range SortKeys(v.MapKeys()) {
sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited) sk := formatAny(k, keyConf, visited)
sv := formatAny(v.MapIndex(k), subConf, visited) sv := formatAny(v.MapIndex(k), valConf, visited)
ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
} }
s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
@ -149,7 +154,7 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str
continue // Elide zero value fields continue // Elide zero value fields
} }
name := v.Type().Field(i).Name name := v.Type().Field(i).Name
subConf.useStringer = conf.useStringer && isExported(name) subConf.UseStringer = conf.UseStringer
s := formatAny(vv, subConf, visited) s := formatAny(vv, subConf, visited)
ss = append(ss, fmt.Sprintf("%s: %s", name, s)) ss = append(ss, fmt.Sprintf("%s: %s", name, s))
} }
@ -163,14 +168,33 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str
} }
} }
func formatPrimitive(t reflect.Type, v interface{}, conf formatConfig) string { func formatString(s string) string {
if conf.printType && t.PkgPath() != "" { // Use quoted string if it the same length as a raw string literal.
// Otherwise, attempt to use the raw string form.
qs := strconv.Quote(s)
if len(qs) == 1+len(s)+1 {
return qs
}
// Disallow newlines to ensure output is a single line.
// Only allow printable runes for readability purposes.
rawInvalid := func(r rune) bool {
return r == '`' || r == '\n' || !unicode.IsPrint(r)
}
if strings.IndexFunc(s, rawInvalid) < 0 {
return "`" + s + "`"
}
return qs
}
func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string {
if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") {
return fmt.Sprintf("%v(%v)", t, v) return fmt.Sprintf("%v(%v)", t, v)
} }
return fmt.Sprintf("%v", v) return fmt.Sprintf("%v", v)
} }
func formatPointer(v reflect.Value, conf formatConfig) string { func formatPointer(v reflect.Value, conf FormatConfig) string {
p := v.Pointer() p := v.Pointer()
if !conf.realPointers { if !conf.realPointers {
p = 0 // For deterministic printing purposes p = 0 // For deterministic printing purposes
@ -251,9 +275,3 @@ func isZero(v reflect.Value) bool {
} }
return false return false
} }
// isExported reports whether the identifier is exported.
func isExported(id string) bool {
r, _ := utf8.DecodeRuneInString(id)
return unicode.IsUpper(r)
}

View File

@ -24,7 +24,7 @@ func SortKeys(vs []reflect.Value) []reflect.Value {
// Deduplicate keys (fails for NaNs). // Deduplicate keys (fails for NaNs).
vs2 := vs[:1] vs2 := vs[:1]
for _, v := range vs[1:] { for _, v := range vs[1:] {
if v.Interface() != vs2[len(vs2)-1].Interface() { if isLess(vs2[len(vs2)-1], v) {
vs2 = append(vs2, v) vs2 = append(vs2, v)
} }
} }

View File

@ -38,9 +38,8 @@ type Option interface {
type applicableOption interface { type applicableOption interface {
Option Option
// apply executes the option and reports whether the option was applied. // apply executes the option, which may mutate s or panic.
// Each option may mutate s. apply(s *state, vx, vy reflect.Value)
apply(s *state, vx, vy reflect.Value) bool
} }
// coreOption represents the following types: // coreOption represents the following types:
@ -85,7 +84,7 @@ func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out
return out return out
} }
func (opts Options) apply(s *state, _, _ reflect.Value) bool { func (opts Options) apply(s *state, _, _ reflect.Value) {
const warning = "ambiguous set of applicable options" const warning = "ambiguous set of applicable options"
const help = "consider using filters to ensure at most one Comparer or Transformer may apply" const help = "consider using filters to ensure at most one Comparer or Transformer may apply"
var ss []string var ss []string
@ -196,7 +195,7 @@ type ignore struct{ core }
func (ignore) isFiltered() bool { return false } func (ignore) isFiltered() bool { return false }
func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} }
func (ignore) apply(_ *state, _, _ reflect.Value) bool { return true } func (ignore) apply(_ *state, _, _ reflect.Value) { return }
func (ignore) String() string { return "Ignore()" } func (ignore) String() string { return "Ignore()" }
// invalid is a sentinel Option type to indicate that some options could not // invalid is a sentinel Option type to indicate that some options could not
@ -204,7 +203,7 @@ func (ignore) String() string
type invalid struct{ core } type invalid struct{ core }
func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} }
func (invalid) apply(s *state, _, _ reflect.Value) bool { func (invalid) apply(s *state, _, _ reflect.Value) {
const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported"
panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
} }
@ -215,9 +214,12 @@ func (invalid) apply(s *state, _, _ reflect.Value) bool {
// The transformer f must be a function "func(T) R" that converts values of // The transformer f must be a function "func(T) R" that converts values of
// type T to those of type R and is implicitly filtered to input values // type T to those of type R and is implicitly filtered to input values
// assignable to T. The transformer must not mutate T in any way. // assignable to T. The transformer must not mutate T in any way.
// If T and R are the same type, an additional filter must be applied to //
// act as the base case to prevent an infinite recursion applying the same // To help prevent some cases of infinite recursive cycles applying the
// transform to itself (see the SortedSlice example). // same transform to the output of itself (e.g., in the case where the
// input and output types are the same), an implicit filter is added such that
// a transformer is applicable only if that exact transformer is not already
// in the tail of the Path since the last non-Transform step.
// //
// The name is a user provided label that is used as the Transform.Name in the // The name is a user provided label that is used as the Transform.Name in the
// transformation PathStep. If empty, an arbitrary name is used. // transformation PathStep. If empty, an arbitrary name is used.
@ -248,14 +250,21 @@ type transformer struct {
func (tr *transformer) isFiltered() bool { return tr.typ != nil } func (tr *transformer) isFiltered() bool { return tr.typ != nil }
func (tr *transformer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption {
for i := len(s.curPath) - 1; i >= 0; i-- {
if t, ok := s.curPath[i].(*transform); !ok {
break // Hit most recent non-Transform step
} else if tr == t.trans {
return nil // Cannot directly use same Transform
}
}
if tr.typ == nil || t.AssignableTo(tr.typ) { if tr.typ == nil || t.AssignableTo(tr.typ) {
return tr return tr
} }
return nil return nil
} }
func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool { func (tr *transformer) apply(s *state, vx, vy reflect.Value) {
// Update path before calling the Transformer so that dynamic checks // Update path before calling the Transformer so that dynamic checks
// will use the updated path. // will use the updated path.
s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr})
@ -264,7 +273,6 @@ func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool {
vx = s.callTRFunc(tr.fnc, vx) vx = s.callTRFunc(tr.fnc, vx)
vy = s.callTRFunc(tr.fnc, vy) vy = s.callTRFunc(tr.fnc, vy)
s.compareAny(vx, vy) s.compareAny(vx, vy)
return true
} }
func (tr transformer) String() string { func (tr transformer) String() string {
@ -310,10 +318,9 @@ func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applica
return nil return nil
} }
func (cm *comparer) apply(s *state, vx, vy reflect.Value) bool { func (cm *comparer) apply(s *state, vx, vy reflect.Value) {
eq := s.callTTBFunc(cm.fnc, vx, vy) eq := s.callTTBFunc(cm.fnc, vx, vy)
s.report(eq, vx, vy) s.report(eq, vx, vy)
return true
} }
func (cm comparer) String() string { func (cm comparer) String() string {
@ -348,7 +355,7 @@ func (cm comparer) String() string {
// all unexported fields on specified struct types. // all unexported fields on specified struct types.
func AllowUnexported(types ...interface{}) Option { func AllowUnexported(types ...interface{}) Option {
if !supportAllowUnexported { if !supportAllowUnexported {
panic("AllowUnexported is not supported on App Engine Classic or GopherJS") panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS")
} }
m := make(map[reflect.Type]bool) m := make(map[reflect.Type]bool)
for _, typ := range types { for _, typ := range types {

View File

@ -79,6 +79,11 @@ type (
PathStep PathStep
Name() string Name() string
Func() reflect.Value Func() reflect.Value
// Option returns the originally constructed Transformer option.
// The == operator can be used to detect the exact option used.
Option() Option
isTransform() isTransform()
} }
) )
@ -94,11 +99,22 @@ func (pa *Path) pop() {
// Last returns the last PathStep in the Path. // Last returns the last PathStep in the Path.
// If the path is empty, this returns a non-nil PathStep that reports a nil Type. // If the path is empty, this returns a non-nil PathStep that reports a nil Type.
func (pa Path) Last() PathStep { func (pa Path) Last() PathStep {
if len(pa) > 0 { return pa.Index(-1)
return pa[len(pa)-1]
} }
// Index returns the ith step in the Path and supports negative indexing.
// A negative index starts counting from the tail of the Path such that -1
// refers to the last step, -2 refers to the second-to-last step, and so on.
// If index is invalid, this returns a non-nil PathStep that reports a nil Type.
func (pa Path) Index(i int) PathStep {
if i < 0 {
i = len(pa) + i
}
if i < 0 || i >= len(pa) {
return pathStep{} return pathStep{}
} }
return pa[i]
}
// String returns the simplified path to a node. // String returns the simplified path to a node.
// The simplified path only contains struct field accesses. // The simplified path only contains struct field accesses.
@ -150,13 +166,12 @@ func (pa Path) GoString() string {
ssPost = append(ssPost, ")") ssPost = append(ssPost, ")")
continue continue
case *typeAssertion: case *typeAssertion:
// Elide type assertions immediately following a transform to // As a special-case, elide type assertions on anonymous types
// prevent overly verbose path printouts. // since they are typically generated dynamically and can be very
// Some transforms return interface{} because of Go's lack of // verbose. For example, some transforms return interface{} because
// generics, but typically take in and return the exact same // of Go's lack of generics, but typically take in and return the
// concrete type. Other times, the transform creates an anonymous // exact same concrete type.
// struct, which will be very verbose to print. if s.Type().PkgPath() == "" {
if _, ok := nextStep.(*transform); ok {
continue continue
} }
} }
@ -250,6 +265,7 @@ func (sf structField) Name() string { return sf.name }
func (sf structField) Index() int { return sf.idx } func (sf structField) Index() int { return sf.idx }
func (tf transform) Name() string { return tf.trans.name } func (tf transform) Name() string { return tf.trans.name }
func (tf transform) Func() reflect.Value { return tf.trans.fnc } func (tf transform) Func() reflect.Value { return tf.trans.fnc }
func (tf transform) Option() Option { return tf.trans }
func (pathStep) isPathStep() {} func (pathStep) isPathStep() {}
func (sliceIndex) isSliceIndex() {} func (sliceIndex) isSliceIndex() {}

View File

@ -30,12 +30,12 @@ func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) {
const maxLines = 256 const maxLines = 256
r.ndiffs++ r.ndiffs++
if r.nbytes < maxBytes && r.nlines < maxLines { if r.nbytes < maxBytes && r.nlines < maxLines {
sx := value.Format(x, true) sx := value.Format(x, value.FormatConfig{UseStringer: true})
sy := value.Format(y, true) sy := value.Format(y, value.FormatConfig{UseStringer: true})
if sx == sy { if sx == sy {
// Stringer is not helpful, so rely on more exact formatting. // Unhelpful output, so use more exact formatting.
sx = value.Format(x, false) sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true})
sy = value.Format(y, false) sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true})
} }
s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
r.diffs = append(r.diffs, s) r.diffs = append(r.diffs, s)
@ -49,5 +49,5 @@ func (r *defaultReporter) String() string {
if r.ndiffs == len(r.diffs) { if r.ndiffs == len(r.diffs) {
return s return s
} }
return fmt.Sprintf("%s... %d more differences ...", s, len(r.diffs)-r.ndiffs) return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs))
} }

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file. // license that can be found in the LICENSE.md file.
// +build appengine js // +build purego appengine js
package cmp package cmp

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file. // license that can be found in the LICENSE.md file.
// +build !appengine,!js // +build !purego,!appengine,!js
package cmp package cmp

View File

@ -1,193 +1,4 @@
Copyright 2018 gotest.tools authors
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@
A collection of packages to augment `testing` and support common patterns. A collection of packages to augment `testing` and support common patterns.
[![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://godoc.org/gotest.tools) [![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://godoc.org/gotest.tools)
[![CircleCI](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master) [![CircleCI](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master)
[![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](https://goreportcard.com/report/gotest.tools) [![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](https://goreportcard.com/report/gotest.tools)
@ -29,3 +29,7 @@ A collection of packages to augment `testing` and support common patterns.
* [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - go test runner with custom output * [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - go test runner with custom output
* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces * [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces
* [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time` * [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time`
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).

View File

@ -4,6 +4,7 @@ package cmp // import "gotest.tools/assert/cmp"
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"strings" "strings"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@ -58,6 +59,39 @@ func toResult(success bool, msg string) Result {
return ResultFailure(msg) return ResultFailure(msg)
} }
// RegexOrPattern may be either a *regexp.Regexp or a string that is a valid
// regexp pattern.
type RegexOrPattern interface{}
// Regexp succeeds if value v matches regular expression re.
//
// Example:
// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str))
// r := regexp.MustCompile("^[0-9a-f]{32}$")
// assert.Assert(t, cmp.Regexp(r, str))
func Regexp(re RegexOrPattern, v string) Comparison {
match := func(re *regexp.Regexp) Result {
return toResult(
re.MatchString(v),
fmt.Sprintf("value %q does not match regexp %q", v, re.String()))
}
return func() Result {
switch regex := re.(type) {
case *regexp.Regexp:
return match(regex)
case string:
re, err := regexp.Compile(regex)
if err != nil {
return ResultFailure(err.Error())
}
return match(re)
default:
return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex))
}
}
}
// Equal succeeds if x == y. See assert.Equal for full documentation. // Equal succeeds if x == y. See assert.Equal for full documentation.
func Equal(x, y interface{}) Comparison { func Equal(x, y interface{}) Comparison {
return func() Result { return func() Result {
@ -186,7 +220,7 @@ func Error(err error, message string) Comparison {
return ResultFailure("expected an error, got nil") return ResultFailure("expected an error, got nil")
case err.Error() != message: case err.Error() != message:
return ResultFailure(fmt.Sprintf( return ResultFailure(fmt.Sprintf(
"expected error %q, got %+v", message, err)) "expected error %q, got %s", message, formatErrorMessage(err)))
} }
return ResultSuccess return ResultSuccess
} }
@ -201,12 +235,22 @@ func ErrorContains(err error, substring string) Comparison {
return ResultFailure("expected an error, got nil") return ResultFailure("expected an error, got nil")
case !strings.Contains(err.Error(), substring): case !strings.Contains(err.Error(), substring):
return ResultFailure(fmt.Sprintf( return ResultFailure(fmt.Sprintf(
"expected error to contain %q, got %+v", substring, err)) "expected error to contain %q, got %s", substring, formatErrorMessage(err)))
} }
return ResultSuccess return ResultSuccess
} }
} }
func formatErrorMessage(err error) string {
if _, ok := err.(interface {
Cause() error
}); ok {
return fmt.Sprintf("%q\n%+v", err, err)
}
// This error was not wrapped with github.com/pkg/errors
return fmt.Sprintf("%q", err)
}
// Nil succeeds if obj is a nil interface, pointer, or function. // Nil succeeds if obj is a nil interface, pointer, or function.
// //
// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, // Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices,

View File

@ -9,31 +9,37 @@ import (
"gotest.tools/internal/source" "gotest.tools/internal/source"
) )
// Result of a Comparison. // A Result of a Comparison.
type Result interface { type Result interface {
Success() bool Success() bool
} }
type result struct { // StringResult is an implementation of Result that reports the error message
// string verbatim and does not provide any templating or formatting of the
// message.
type StringResult struct {
success bool success bool
message string message string
} }
func (r result) Success() bool { // Success returns true if the comparison was successful.
func (r StringResult) Success() bool {
return r.success return r.success
} }
func (r result) FailureMessage() string { // FailureMessage returns the message used to provide additional information
// about the failure.
func (r StringResult) FailureMessage() string {
return r.message return r.message
} }
// ResultSuccess is a constant which is returned by a ComparisonWithResult to // ResultSuccess is a constant which is returned by a ComparisonWithResult to
// indicate success. // indicate success.
var ResultSuccess = result{success: true} var ResultSuccess = StringResult{success: true}
// ResultFailure returns a failed Result with a failure message. // ResultFailure returns a failed Result with a failure message.
func ResultFailure(message string) Result { func ResultFailure(message string) StringResult {
return result{message: message} return StringResult{message: message}
} }
// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure // ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure

View File

@ -70,7 +70,6 @@ func filterPrintableExpr(args []ast.Expr) []ast.Expr {
result[i] = starExpr.X result[i] = starExpr.X
continue continue
} }
result[i] = nil
} }
return result return result
} }

View File

@ -20,12 +20,14 @@ func max(a, b int) int {
return b return b
} }
// Match stores line numbers of size of match
type Match struct { type Match struct {
A int A int
B int B int
Size int Size int
} }
// OpCode identifies the type of diff
type OpCode struct { type OpCode struct {
Tag byte Tag byte
I1 int I1 int
@ -73,19 +75,20 @@ type SequenceMatcher struct {
opCodes []OpCode opCodes []OpCode
} }
// NewMatcher returns a new SequenceMatcher
func NewMatcher(a, b []string) *SequenceMatcher { func NewMatcher(a, b []string) *SequenceMatcher {
m := SequenceMatcher{autoJunk: true} m := SequenceMatcher{autoJunk: true}
m.SetSeqs(a, b) m.SetSeqs(a, b)
return &m return &m
} }
// Set two sequences to be compared. // SetSeqs sets two sequences to be compared.
func (m *SequenceMatcher) SetSeqs(a, b []string) { func (m *SequenceMatcher) SetSeqs(a, b []string) {
m.SetSeq1(a) m.SetSeq1(a)
m.SetSeq2(b) m.SetSeq2(b)
} }
// Set the first sequence to be compared. The second sequence to be compared is // SetSeq1 sets the first sequence to be compared. The second sequence to be compared is
// not changed. // not changed.
// //
// SequenceMatcher computes and caches detailed information about the second // SequenceMatcher computes and caches detailed information about the second
@ -103,7 +106,7 @@ func (m *SequenceMatcher) SetSeq1(a []string) {
m.opCodes = nil m.opCodes = nil
} }
// Set the second sequence to be compared. The first sequence to be compared is // SetSeq2 sets the second sequence to be compared. The first sequence to be compared is
// not changed. // not changed.
func (m *SequenceMatcher) SetSeq2(b []string) { func (m *SequenceMatcher) SetSeq2(b []string) {
if &b == &m.b { if &b == &m.b {
@ -129,12 +132,12 @@ func (m *SequenceMatcher) chainB() {
m.bJunk = map[string]struct{}{} m.bJunk = map[string]struct{}{}
if m.IsJunk != nil { if m.IsJunk != nil {
junk := m.bJunk junk := m.bJunk
for s, _ := range b2j { for s := range b2j {
if m.IsJunk(s) { if m.IsJunk(s) {
junk[s] = struct{}{} junk[s] = struct{}{}
} }
} }
for s, _ := range junk { for s := range junk {
delete(b2j, s) delete(b2j, s)
} }
} }
@ -149,7 +152,7 @@ func (m *SequenceMatcher) chainB() {
popular[s] = struct{}{} popular[s] = struct{}{}
} }
} }
for s, _ := range popular { for s := range popular {
delete(b2j, s) delete(b2j, s)
} }
} }
@ -259,7 +262,7 @@ func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
return Match{A: besti, B: bestj, Size: bestsize} return Match{A: besti, B: bestj, Size: bestsize}
} }
// Return list of triples describing matching subsequences. // GetMatchingBlocks returns a list of triples describing matching subsequences.
// //
// Each triple is of the form (i, j, n), and means that // Each triple is of the form (i, j, n), and means that
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
@ -323,7 +326,7 @@ func (m *SequenceMatcher) GetMatchingBlocks() []Match {
return m.matchingBlocks return m.matchingBlocks
} }
// Return list of 5-tuples describing how to turn a into b. // GetOpCodes returns a list of 5-tuples describing how to turn a into b.
// //
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
@ -374,7 +377,7 @@ func (m *SequenceMatcher) GetOpCodes() []OpCode {
return m.opCodes return m.opCodes
} }
// Isolate change clusters by eliminating ranges with no changes. // GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes.
// //
// Return a generator of groups with up to n lines of context. // Return a generator of groups with up to n lines of context.
// Each group is in the same format as returned by GetOpCodes(). // Each group is in the same format as returned by GetOpCodes().
@ -384,7 +387,7 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
} }
codes := m.GetOpCodes() codes := m.GetOpCodes()
if len(codes) == 0 { if len(codes) == 0 {
codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} codes = []OpCode{{'e', 0, 1, 0, 1}}
} }
// Fixup leading and trailing groups if they show no changes. // Fixup leading and trailing groups if they show no changes.
if codes[0].Tag == 'e' { if codes[0].Tag == 'e' {

View File

@ -0,0 +1,53 @@
package source
import (
"go/ast"
"go/token"
"github.com/pkg/errors"
)
func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
var matchedNode ast.Node
ast.Inspect(node, func(node ast.Node) bool {
switch {
case node == nil || matchedNode != nil:
return false
case fileset.Position(node.End()).Line == lineNum:
if funcLit, ok := node.(*ast.FuncLit); ok {
matchedNode = funcLit
return false
}
}
return true
})
debug("defer line node: %s", debugFormatNode{matchedNode})
return matchedNode
}
func guessDefer(node ast.Node) (ast.Node, error) {
defers := collectDefers(node)
switch len(defers) {
case 0:
return nil, errors.New("failed to expression in defer")
case 1:
return defers[0].Call, nil
default:
return nil, errors.Errorf(
"ambiguous call expression: multiple (%d) defers in call block",
len(defers))
}
}
func collectDefers(node ast.Node) []*ast.DeferStmt {
var defers []*ast.DeferStmt
ast.Inspect(node, func(node ast.Node) bool {
if d, ok := node.(*ast.DeferStmt); ok {
defers = append(defers, d)
debug("defer: %s", debugFormatNode{d})
return false
}
return true
})
return defers
}

View File

@ -24,106 +24,12 @@ func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
if argPos >= len(args) {
return "", errors.New("failed to find expression")
}
return FormatNode(args[argPos]) return FormatNode(args[argPos])
} }
func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse source file: %s", filename)
}
node := scanToLine(fileset, astFile, lineNum)
if node == nil {
return nil, errors.Errorf(
"failed to find an expression on line %d in %s", lineNum, filename)
}
return node, nil
}
func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset}
ast.Walk(v, node)
return v.matchedNode
}
type scanToLineVisitor struct {
lineNum int
matchedNode ast.Node
fileset *token.FileSet
}
func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor {
if node == nil || v.matchedNode != nil {
return nil
}
if v.nodePosition(node).Line == v.lineNum {
v.matchedNode = node
return nil
}
return v
}
// In golang 1.9 the line number changed from being the line where the statement
// ended to the line where the statement began.
func (v *scanToLineVisitor) nodePosition(node ast.Node) token.Position {
if goVersionBefore19 {
return v.fileset.Position(node.End())
}
return v.fileset.Position(node.Pos())
}
var goVersionBefore19 = isGOVersionBefore19()
func isGOVersionBefore19() bool {
version := runtime.Version()
// not a release version
if !strings.HasPrefix(version, "go") {
return false
}
version = strings.TrimPrefix(version, "go")
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
minor, err := strconv.ParseInt(parts[1], 10, 32)
return err == nil && parts[0] == "1" && minor < 9
}
func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
visitor := &callExprVisitor{}
ast.Walk(visitor, node)
if visitor.expr == nil {
return nil, errors.New("failed to find call expression")
}
return visitor.expr.Args, nil
}
type callExprVisitor struct {
expr *ast.CallExpr
}
func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
if v.expr != nil || node == nil {
return nil
}
debug("visit (%T): %s", node, debugFormatNode{node})
if callExpr, ok := node.(*ast.CallExpr); ok {
v.expr = callExpr
return nil
}
return v
}
// FormatNode using go/format.Node and return the result as a string
func FormatNode(node ast.Node) (string, error) {
buf := new(bytes.Buffer)
err := format.Node(buf, token.NewFileSet(), node)
return buf.String(), err
}
// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at // CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at
// the index in the call stack. // the index in the call stack.
func CallExprArgs(stackIndex int) ([]ast.Expr, error) { func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
@ -137,12 +43,109 @@ func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
debug("found node (%T): %s", node, debugFormatNode{node}) debug("found node: %s", debugFormatNode{node})
return getCallExprArgs(node) return getCallExprArgs(node)
} }
var debugEnabled = os.Getenv("GOTESTYOURSELF_DEBUG") != "" func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse source file: %s", filename)
}
if node := scanToLine(fileset, astFile, lineNum); node != nil {
return node, nil
}
if node := scanToDeferLine(fileset, astFile, lineNum); node != nil {
node, err := guessDefer(node)
if err != nil || node != nil {
return node, err
}
}
return nil, errors.Errorf(
"failed to find an expression on line %d in %s", lineNum, filename)
}
func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
var matchedNode ast.Node
ast.Inspect(node, func(node ast.Node) bool {
switch {
case node == nil || matchedNode != nil:
return false
case nodePosition(fileset, node).Line == lineNum:
matchedNode = node
return false
}
return true
})
return matchedNode
}
// In golang 1.9 the line number changed from being the line where the statement
// ended to the line where the statement began.
func nodePosition(fileset *token.FileSet, node ast.Node) token.Position {
if goVersionBefore19 {
return fileset.Position(node.End())
}
return fileset.Position(node.Pos())
}
var goVersionBefore19 = func() bool {
version := runtime.Version()
// not a release version
if !strings.HasPrefix(version, "go") {
return false
}
version = strings.TrimPrefix(version, "go")
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
minor, err := strconv.ParseInt(parts[1], 10, 32)
return err == nil && parts[0] == "1" && minor < 9
}()
func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
visitor := &callExprVisitor{}
ast.Walk(visitor, node)
if visitor.expr == nil {
return nil, errors.New("failed to find call expression")
}
debug("callExpr: %s", debugFormatNode{visitor.expr})
return visitor.expr.Args, nil
}
type callExprVisitor struct {
expr *ast.CallExpr
}
func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
if v.expr != nil || node == nil {
return nil
}
debug("visit: %s", debugFormatNode{node})
switch typed := node.(type) {
case *ast.CallExpr:
v.expr = typed
return nil
case *ast.DeferStmt:
ast.Walk(v, typed.Call.Fun)
return nil
}
return v
}
// FormatNode using go/format.Node and return the result as a string
func FormatNode(node ast.Node) (string, error) {
buf := new(bytes.Buffer)
err := format.Node(buf, token.NewFileSet(), node)
return buf.String(), err
}
var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != ""
func debug(format string, args ...interface{}) { func debug(format string, args ...interface{}) {
if debugEnabled { if debugEnabled {
@ -159,5 +162,5 @@ func (n debugFormatNode) String() string {
if err != nil { if err != nil {
return fmt.Sprintf("failed to format %s: %s", n.Node, err) return fmt.Sprintf("failed to format %s: %s", n.Node, err)
} }
return out return fmt.Sprintf("(%T) %s", n.Node, out)
} }