diff --git a/.github/workflows/go.yml b/.github/workflows/go-test-matrix.yml similarity index 78% rename from .github/workflows/go.yml rename to .github/workflows/go-test-matrix.yml index 5177bca..3ed7800 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go-test-matrix.yml @@ -1,13 +1,10 @@ -# This workflow will build a golang project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go - -name: Go +name: "Go Test Matrix" on: push: - branches: [ "master" ] + branches: [ "main" ] pull_request: - branches: [ "master" ] + branches: [ "main" ] jobs: test: diff --git a/.gitignore b/.gitignore index 53f12f0..5f18ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.swp *.out +coverage.* diff --git a/README.md b/README.md index b4f723c..7fff6cb 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,106 @@ # Deep Variable Equality for Humans -[![Coverage Status](https://coveralls.io/repos/github/go-test/deep/badge.svg?branch=master)](https://coveralls.io/github/go-test/deep?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/go-test/deep/badge.svg?branch=main)](https://coveralls.io/github/go-test/deep?branch=main) [![Go Reference](https://pkg.go.dev/badge/github.com/go-test/deep.svg)](https://pkg.go.dev/github.com/go-test/deep) -This package provides a single function: `deep.Equal`. It's like [reflect.DeepEqual](https://pkg.go.dev/reflect#DeepEqual) but friendlier to humans for two reason: - -* `deep.Equal` returns a list of differences -* `deep.Equal` does not compare unexported fields (by default) - -`reflect.DeepEqual` is good (like all things Golang!), but it's a game of [Hunt the Wumpus](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). For large maps, slices, and structs, finding the difference is difficult. - -`deep.Equal` doesn't play games with you, it lists the differences: +This package provides deep variable equality testing for tired humans and indefatigable AI. +Whereas [reflect.DeepEqual](https://pkg.go.dev/reflect#DeepEqual) says only true or false, go-test/deep says where deeply nested values differ. ```go package main_test import ( - "testing" - "github.com/go-test/deep" + "testing" + "github.com/go-test/deep" ) type T struct { - Name string - Numbers []float64 + Name string + Numbers []float64 } func TestDeepEqual(t *testing.T) { - // Can you spot the difference? - t1 := T{ - Name: "Isabella", - Numbers: []float64{1.13459, 2.29343, 3.010100010}, - } - t2 := T{ - Name: "Isabella", - Numbers: []float64{1.13459, 2.29843, 3.010100010}, - } - - if diff := deep.Equal(t1, t2); diff != nil { - t.Error(diff) - } + a := T{ + Name: "Isabella", + Numbers: []float64{1.13459, 2.29343, 3.010100010}, + } + b := T{ + Name: "Isabella", + Numbers: []float64{1.13459, 2.29843, 3.010100010}, + } + d := deep.Testing(t) + d.Equal(a, b) } ``` +Run `go test` and you get: ``` -$ go test --- FAIL: TestDeepEqual (0.00s) - main_test.go:25: [Numbers.slice[1]: 2.29343 != 2.29843] + main_test.go:23: [Numbers.slice[1]: 2.29343 != 2.29843] ``` The difference is in `Numbers.slice[1]`: the two values aren't equal using Go `==`. + +## New Usage (v1.2) + +As of go-test/deep v1.2, use the `Differ` type for parallel test runs and better future-proofing: + +```go +import "github.com/go-test/deep" + +func compare() { + d := deep.NewDiffer() + a := "foo" + b := "bar" + if diff := d.Equal(a, b); diff != nil { + // do something + } +} +``` + +But since go-test/deep is almost always used in unit tests, there's a simpler way: + +```go +func TestFoo(t *testing.T) { + d := deep.Testing(t) + a := "foo" + b := "bar" + d.Equal(a, b) +} +``` + +`deep.Testing` is just a wrapper around `deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{T: t}))`. +When `d.Equal` finds a diff, inline testing reports it at that line using `t.Errorf`. +To add context to the failure message, use `d.Equalf(a, b, "test case %d failed: {diff}", i)`. + +## Classic Usage + +Before v1.2 this package exported only one function: `deep.Equal`. +It still does for backward compatibility. + +```go +import "github.com/go-test/deep" + +func compare() { + a := "foo" + b := "bar" + if diff := deep.Equal(a, b); diff != nil { + // do something + } +} +``` + +The package function `deep.Equal` and a default `Differ` (no options) are functionally equivalent, but prefer `Differ`. + +## Contributing + +Human and AI contributions welcome, just be sure to: + +1. Make minimal, focused, tactical changes +2. Add test cases to `deep_test.go` (or `differ_test.go` if specific to `Differ`) +3. Maintain 100% test coverage +4. Preserve API compatibility +5. Document the "what and why" + +As always with free open source projects, issues and PRs do not guarantee adoption, and those that are merged might be tweaked and changed before release. diff --git a/deep.go b/deep.go index 7fb0f7f..44254ea 100644 --- a/deep.go +++ b/deep.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "reflect" + "slices" "strings" ) @@ -83,55 +84,43 @@ type cmp struct { diff []string buff []string floatFormat string - flag map[byte]bool + conf Differ active map[pointerVisit]bool } -var errorType = reflect.TypeOf((*error)(nil)).Elem() +var errorType = reflect.TypeFor[error]() // https://blog.carlana.net/post/2024/golang-reflect-type-for/ -// Equal compares variables a and b, recursing into their structure up to -// MaxDepth levels deep (if greater than zero), and returns a list of differences, -// or nil if there are none. Some differences may not be found if an error is -// also returned. +// Equal returns the set of differences between a and b, or nil if none found. // -// If a type has an Equal method, like time.Equal, it is called to check for -// equality. -// -// When comparing a struct, if a field has the tag `deep:"-"` then it will be -// ignored. -func Equal(a, b interface{}, flags ...interface{}) []string { - aVal := reflect.ValueOf(a) - bVal := reflect.ValueOf(b) - c := &cmp{ - diff: []string{}, - buff: []string{}, - floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), - flag: map[byte]bool{}, - } - for i := range flags { - c.flag[flags[i].(byte)] = true - } - if a == nil && b == nil { - return nil - } else if a == nil && b != nil { - c.saveDiff("", b) - } else if a != nil && b == nil { - c.saveDiff(a, "") - } - if len(c.diff) > 0 { - return c.diff - } +// This is the original package function that, as of v1.2, has been superseded +// by Differ. It's functionally equivalent to a Differ with default values (no +// options). +func Equal(a, b any, flags ...any) []string { + // error ignored to preserve API + return NewDiffer( + WithCompareFunctions(CompareFunctions), + WithCompareUnexportedFields(CompareUnexportedFields), + WithFloatPrecision(uint(FloatPrecision)), + WithIgnoreSliceOrder(hasFlag(flags, FLAG_IGNORE_SLICE_ORDER)), + WithLogErrors(LogErrors), + WithMaxDepth(uint(MaxDepth)), + WithMaxDiff(uint(MaxDiff)), + WithNilMapsAreEmpty(NilMapsAreEmpty), + WithNilPointersAreZero(NilPointersAreZero), + WithNilSlicesAreEmpty(NilSlicesAreEmpty), + ).diff(a, b) +} - c.equals(aVal, bVal, 0) - if len(c.diff) > 0 { - return c.diff // diffs - } - return nil // no diffs +func hasFlag(flags []any, flag byte) bool { + return slices.ContainsFunc(flags, func(i any) bool { + v, ok := i.(byte) + return ok && v == flag + }) } func (c *cmp) equals(a, b reflect.Value, level int) { - if MaxDepth > 0 && level > MaxDepth { - logError(ErrMaxRecursion) + if c.conf.maxDepth > 0 && uint(level) > c.conf.maxDepth { + c.logError(ErrMaxRecursion) return } @@ -161,7 +150,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { bFullType := bType.PkgPath() + "." + bType.Name() c.saveDiff(aFullType, bFullType) } - logError(ErrTypeMismatch) + c.logError(ErrTypeMismatch) return } @@ -170,8 +159,8 @@ func (c *cmp) equals(a, b reflect.Value, level int) { bKind := b.Kind() // Do a and b have underlying elements? Yes if they're ptr or interface. - aElem := aKind == reflect.Ptr || aKind == reflect.Interface - bElem := bKind == reflect.Ptr || bKind == reflect.Interface + aElem := aKind == reflect.Pointer || aKind == reflect.Interface + bElem := bKind == reflect.Pointer || bKind == reflect.Interface // If both types implement the error interface, compare the error strings. // This must be done before dereferencing because errors.New() returns a @@ -215,10 +204,10 @@ func (c *cmp) equals(a, b reflect.Value, level int) { if bElem { b = b.Elem() } - if aElem && NilPointersAreZero && !a.IsValid() && b.IsValid() { + if aElem && c.conf.nilPointersAreZero && !a.IsValid() && b.IsValid() { a = reflect.Zero(b.Type()) } - if bElem && NilPointersAreZero && !b.IsValid() && a.IsValid() { + if bElem && c.conf.nilPointersAreZero && !b.IsValid() && a.IsValid() { b = reflect.Zero(a.Type()) } c.equals(a, b, level+1) @@ -267,7 +256,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { */ for i := 0; i < a.NumField(); i++ { - if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { + if aType.Field(i).PkgPath != "" && !c.conf.compareUnexportedFields { continue // skip unexported field, e.g. s in type T struct {s string} } @@ -287,7 +276,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { c.pop() // pop field name from buff - if len(c.diff) >= MaxDiff { + if uint(len(c.diff)) >= c.conf.maxDiff { break } } @@ -308,7 +297,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { */ if a.IsNil() || b.IsNil() { - if NilMapsAreEmpty { + if c.conf.nilMapsAreEmpty { if a.IsNil() && b.Len() != 0 { c.saveDiff("", b) return @@ -343,7 +332,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { c.pop() - if len(c.diff) >= MaxDiff { + if uint(len(c.diff)) >= c.conf.maxDiff { return } } @@ -356,22 +345,22 @@ func (c *cmp) equals(a, b reflect.Value, level int) { c.push(fmt.Sprintf("map[%v]", key)) c.saveDiff("", b.MapIndex(key)) c.pop() - if len(c.diff) >= MaxDiff { + if uint(len(c.diff)) >= c.conf.maxDiff { return } } case reflect.Array: n := a.Len() - for i := 0; i < n; i++ { + for i := range n { c.push(fmt.Sprintf("array[%d]", i)) c.equals(a.Index(i), b.Index(i), level+1) c.pop() - if len(c.diff) >= MaxDiff { + if uint(len(c.diff)) >= c.conf.maxDiff { break } } case reflect.Slice: - if NilSlicesAreEmpty { + if c.conf.nilSlicesAreEmpty { if a.IsNil() && b.Len() != 0 { c.saveDiff("", b) return @@ -401,30 +390,26 @@ func (c *cmp) equals(a, b reflect.Value, level int) { return } - if c.flag[FLAG_IGNORE_SLICE_ORDER] { + if c.conf.ignoreSliceOrder { // Compare slices by value and value count; ignore order. // Value equality is implicitly established by the maps: // any value v1 will hash to the same map value if it's equal // to another value v2. Then equality is determined by value // count: presuming v1==v2, then the slices are equal if there // are equal numbers of v1 in each slice. - am := map[interface{}]int{} + am := map[any]int{} for i := 0; i < a.Len(); i++ { am[a.Index(i).Interface()] += 1 } - bm := map[interface{}]int{} + bm := map[any]int{} for i := 0; i < b.Len(); i++ { bm[b.Index(i).Interface()] += 1 } - c.cmpMapValueCounts(a, b, am, bm, true) // a cmp b - c.cmpMapValueCounts(b, a, bm, am, false) // b cmp a + c.cmpMapValueCounts(am, bm, true) // a cmp b + c.cmpMapValueCounts(bm, am, false) // b cmp a } else { // Compare slices by order - n := aLen - if bLen > aLen { - n = bLen - } - for i := 0; i < n; i++ { + for i := range max(aLen, bLen) { c.push(fmt.Sprintf("slice[%d]", i)) if i < aLen && i < bLen { c.equals(a.Index(i), b.Index(i), level+1) @@ -434,7 +419,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { c.saveDiff("", b.Index(i)) } c.pop() - if len(c.diff) >= MaxDiff { + if uint(len(c.diff)) >= c.conf.maxDiff { break } } @@ -479,7 +464,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { c.saveDiff(a.String(), b.String()) } case reflect.Func: - if CompareFunctions { + if c.conf.compareFunctions { if !a.IsNil() || !b.IsNil() { aVal, bVal := "nil func", "nil func" if !a.IsNil() { @@ -492,7 +477,7 @@ func (c *cmp) equals(a, b reflect.Value, level int) { } } default: - logError(ErrNotHandled) + c.logError(ErrNotHandled) } } @@ -506,7 +491,7 @@ func (c *cmp) pop() { } } -func (c *cmp) saveDiff(aval, bval interface{}) { +func (c *cmp) saveDiff(aval, bval any) { if len(c.buff) > 0 { varName := strings.Join(c.buff, ".") c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) @@ -515,7 +500,7 @@ func (c *cmp) saveDiff(aval, bval interface{}) { } } -func (c *cmp) cmpMapValueCounts(a, b reflect.Value, am, bm map[interface{}]int, a2b bool) { +func (c *cmp) cmpMapValueCounts(am, bm map[any]int, a2b bool) { for v := range am { aCount, _ := am[v] bCount, _ := bm[v] @@ -534,8 +519,8 @@ func (c *cmp) cmpMapValueCounts(a, b reflect.Value, am, bm map[interface{}]int, } } -func logError(err error) { - if LogErrors { +func (c *cmp) logError(err error) { + if c.conf.logErrors { log.Println(err) } } diff --git a/deep_test.go b/deep_test.go index 8ea31ec..5c20f56 100644 --- a/deep_test.go +++ b/deep_test.go @@ -1203,12 +1203,12 @@ func TestTimeUnexported(t *testing.T) { } func TestInterface(t *testing.T) { - a := map[string]interface{}{ + a := map[string]any{ "foo": map[string]string{ "bar": "a", }, } - b := map[string]interface{}{ + b := map[string]any{ "foo": map[string]string{ "bar": "b", }, @@ -1229,10 +1229,10 @@ func TestInterface2(t *testing.T) { } }() - a := map[string]interface{}{ + a := map[string]any{ "bar": 1, } - b := map[string]interface{}{ + b := map[string]any{ "bar": 1.23, } diff := deep.Equal(a, b) @@ -1246,10 +1246,10 @@ func TestInterface2(t *testing.T) { func TestInterface3(t *testing.T) { type Value struct{ int } - a := map[string]interface{}{ + a := map[string]any{ "foo": &Value{}, } - b := map[string]interface{}{ + b := map[string]any{ "foo": 1.23, } diff := deep.Equal(a, b) @@ -1449,7 +1449,7 @@ func TestNil(t *testing.T) { } mark := student{"mark", 10} - var someNilThing interface{} = nil + var someNilThing any = nil diff := deep.Equal(someNilThing, mark) if diff == nil { t.Error("Nil value to comparison should not be equal") diff --git a/differ.go b/differ.go new file mode 100644 index 0000000..74b0bf1 --- /dev/null +++ b/differ.go @@ -0,0 +1,303 @@ +// Package deep provides function deep.Equal which is like reflect.DeepEqual +// but returns a descriptive set of differences. This surfaces differences in +// deeply nested data structures. It also has options for pragmatic equality, +// such as treating empty and nil slices as equal. And it works with testing.T +// to test and report diffs in a single line. +// +// As of v1.2, Differ is canonical usage rather than package function Equal. +// See the Differ type docs for usage. +package deep + +import ( + "fmt" + "reflect" + "strings" +) + +type Opt func(*Differ) + +// WithFloatPrecision is the number of decimal places to round float values +// to when comparing. +func WithFloatPrecision(p uint) Opt { + return func(d *Differ) { + d.floatPrecision = p + } +} + +// WithMaxDiff specifies the maximum number of differences to return. +func WithMaxDiff(m uint) Opt { + return func(d *Differ) { + d.maxDiff = m + } +} + +// WithMaxDepth specifies the maximum levels of a struct to recurse into, +// if greater than zero. If zero, there is no limit. +func WithMaxDepth(m uint) Opt { + return func(d *Differ) { + d.maxDepth = m + } +} + +// WithLogErrors causes errors to be logged to STDERR when true. +func WithLogErrors(b bool) Opt { + return func(d *Differ) { + d.logErrors = b + } +} + +// WithCompareUnexportedFields causes unexported struct fields, like s in +// T{s int}, to be compared when true. This does not work for comparing +// error or Time types on unexported fields because methods on unexported +// fields cannot be called. +func WithCompareUnexportedFields(b bool) Opt { + return func(d *Differ) { + d.compareUnexportedFields = b + } +} + +// WithCompareFunctions compares functions the same as reflect.DeepEqual: +// only two nil functions are equal. Every other combination is not equal. +// This is disabled by default because previous versions of this package +// ignored functions. Enabling it can possibly report new diffs. +func WithCompareFunctions(b bool) Opt { + return func(d *Differ) { + d.compareFunctions = b + } +} + +// WithNilSlicesAreEmpty causes a nil slice to be equal to an empty slice. +func WithNilSlicesAreEmpty(b bool) Opt { + return func(d *Differ) { + d.nilSlicesAreEmpty = b + } +} + +// WithNilMapsAreEmpty causes a nil map to be equal to an empty map. +func WithNilMapsAreEmpty(b bool) Opt { + return func(d *Differ) { + d.nilMapsAreEmpty = b + } +} + +// WithNilPointersAreZero causes a nil pointer to be equal to a zero value. +func WithNilPointersAreZero(b bool) Opt { + return func(d *Differ) { + d.nilPointersAreZero = b + } +} + +// WithIgnoreSliceOrder causes Equal to ignore slice order so that +// []int{1, 2} and []int{2, 1} are equal. Only slices of primitive scalars +// like numbers and strings are supported. Slices of complex types, +// like []T where T is a struct, are undefined because Equal does not +// recurse into the slice value when this flag is enabled. +func WithIgnoreSliceOrder(b bool) Opt { + return func(d *Differ) { + d.ignoreSliceOrder = b + } +} + +// WithInlineTesting makes Equal and Equalf call T.Errorf when diffs are found. +// It also calls T.Helper to make the error report where it's called in the +// unit test, not within Equal/Equalf. This option replaces a common pattern: +// +// if diff := deep.Equal("foo", "bar"); diff != nil { +// t.Error(diff) +// } +// +// Instead, when d is a Differ with inline testing: +// +// d.Equal("foo", "bar") +// +// Or use Equalf to format the test failure output. +func WithInlineTesting(i InlineTesting) Opt { + return func(d *Differ) { d.inline = i } +} + +// InlineTesting is the value passed to WithInlineTesting. Only T is required, +// and notice: it's the deep.T interface, and standard library testing.T +// satisfies this interface. (The deep.T interface is necessary to test the +// testing.) +// +// If specially formatted test failure output is not needed, deep.Testing +// quickly instantiates an inline testing Differ. +type InlineTesting struct { + T T // *testing.T (required) + Fatal bool // call T.Errorf by default + DiffFormatter func(Diff) string // replaces DiffPlaceholder +} + +// DiffPlaceholder is replaced by the formatted diff in the Equalf format. +const DiffPlaceholder = `{diff}` + +// Differ diffs two values and returns a Diff if any are found, else nil. +// The easiest usage is with inline testing: +// +// func TestFoo(t *testing.T) { +// d := deep.Testing(t) +// a := "foo" +// b := "bar" +// d.Equal(a, b) +// } +// +// That unit test will t.Error at the d.Equal line because "foo" != "bar". +// For greater control of the Differ, call NewDiffer with options. If the old +// style (before v1.2) is preferred but parallel Differ are required: +// +// d := deep.NewDiffer() +// if diff := d.Equal(a, b); diff != nil { +// t.Error(diff) +// } +// +// That is functionally equivalent to using the original package function: +// deep.Equal(a, b), which is preserved because it's still widely used. +type Differ struct { + compareFunctions bool + compareUnexportedFields bool + floatPrecision uint + ignoreSliceOrder bool + logErrors bool + maxDepth uint + maxDiff uint + nilMapsAreEmpty bool + nilPointersAreZero bool + nilSlicesAreEmpty bool + inline InlineTesting +} + +// NewDiffer creates a new Differ with default values if no options are given. +// Differ defaults are equivalent to package function Equal. +func NewDiffer(opts ...Opt) Differ { + d := Differ{ + // options where zero-value equals default value are omitted + floatPrecision: 10, + maxDiff: 10, + } + for opt := range opts { + opts[opt](&d) + } + return d +} + +// Testing returns an inline testing Differ. It's a convenience wrapper for: +// +// d := deep.NewDiffer( +// deep.WithInlineTesting(deep.InlineTesting{ +// T: t, +// }), +// ) +func Testing(t T) Differ { + return NewDiffer(WithInlineTesting(InlineTesting{T: t})) +} + +// Diff is a set of differences between a and b, or nil if no diffs were found. +type Diff []string + +// Equal compares a and b and returns a Diff if any are found, else nil. +// With inline testing, the diff is reported. Without inline testing, the +// caller must check if diff != nil and handle if true (e.g. cause a test +// failure with t.Error). +// +// With default options, it recurses without limit and compares every exported +// field, but it stops early after finding WithMaxDiff diffs (default: 10). +// +// When comparing a struct, fields tagged `deep:"-"` are ignored. +// +// If a type has an Equal() bool method, like time.Equal, it's called to check +// for equality. +// +// Some differences may not be found if an error occurs. +func (d Differ) Equal(a, b any) Diff { + if d.inline.T != nil { + d.inline.T.Helper() + } + return d.Equalf(a, b, DiffPlaceholder) +} + +// Equalf is Equal with custom error message formatting. The format and args +// are passed to T.Errorf/T.Fatalf with only one change: DiffPlaceholder is +// replaced with the formatted diff. For example: +// +// d.Equalf(a, b, "test case %d failed: {diff}", i) +// +// Values a and b are not args to the format; they're the values being tested. +// The resulting diff, if any, is represented in the format by DiffPlaceholder. +// Use InlineTesting.DiffFormatter to format the diff before interpolation. +func (d Differ) Equalf(a, b any, format string, args ...any) Diff { + diff := d.diff(a, b) + if diff == nil { + return nil + } + + t := d.inline.T + if t == nil { + return diff + } + + // Format the diff then interpolate with placeholder. Must also s/%/%%/g + // in the diff string so they're literal % in Errorf/Fatalf rather than + // direcctives. + var s string + if d.inline.DiffFormatter != nil { + s = d.inline.DiffFormatter(diff) + } else { + s = fmt.Sprintf("%s", diff) + } + format = strings.ReplaceAll(format, DiffPlaceholder, strings.ReplaceAll(s, `%`, `%%`)) + + // Report diff at caller's line number + t.Helper() + if d.inline.Fatal { + t.Fatalf(format, args...) + } else { + t.Errorf(format, args...) + } + + return diff +} + +// diff returns the set of differences between a and b, or nil if none found. +// With default options, it recurses without limit and compares every exported +// field, but it stops early after finding WithMaxDiff diffs (default: 10). +// +// When comparing a struct, fields tagged `deep:"-"` are ignored. +// +// Some differences may not be found if an error occurs. +func (d Differ) diff(a, b any) Diff { + aVal := reflect.ValueOf(a) + bVal := reflect.ValueOf(b) + c := &cmp{ + conf: d, + diff: []string{}, + buff: []string{}, + floatFormat: fmt.Sprintf("%%.%df", d.floatPrecision), + } + + // Can bypass full diff if both or only one operand is nil + if a == nil && b == nil { + return nil + } else if a == nil && b != nil { + c.saveDiff("", b) + } else if a != nil && b == nil { + c.saveDiff(a, "") + } else { + c.equals(aVal, bVal, 0) // full diff + } + + // Return diffs if any, else return nil--never an empty slice--because + // this package has always been used like: + // if diff := deep.Equal(a, b); diff != nil { + // Also, nil for no diffs is documented. + if len(c.diff) > 0 { + return c.diff // diffs + } + return nil // no diffs +} + +// T is the subset of standard library testing.T that WithInlineTesting needs. +type T interface { + Errorf(format string, args ...any) + Fatalf(format string, args ...any) + Helper() +} diff --git a/differ_test.go b/differ_test.go new file mode 100644 index 0000000..c6b6028 --- /dev/null +++ b/differ_test.go @@ -0,0 +1,123 @@ +package deep_test + +import ( + "fmt" + "testing" + + "github.com/go-test/deep" +) + +func TestDiffer(t *testing.T) { + d := deep.NewDiffer() + + diff1 := d.Equal("foo", "bar") + if diff1 == nil { + t.Error("no diff") + } + + // diff1 and diff3 are NOT equal + diff3 := d.Equal("a", "b") + if diff3 == nil { + t.Error("should not be equal") + } +} + +type mockT struct { + calledError bool + calledFatal bool + calledHelper bool + errFormat string + errArgs []any +} + +func (t *mockT) Errorf(format string, args ...any) { + t.calledError = true + t.errFormat = format + t.errArgs = args +} +func (t *mockT) Fatalf(format string, args ...any) { t.calledFatal = true } +func (t *mockT) Helper() { t.calledHelper = true } + +func TestDiffer_WithInlineTesting(t *testing.T) { + // No diff, no calls to T except Helper + m := &mockT{} + d := deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{T: m})) + d.Equal("foo", "foo") + if m.calledHelper == false { + t.Errorf("did not call T.Helper") + } + if m.calledError == true { + t.Errorf("called T.Error") + } + if m.calledFatal == true { + t.Errorf("called T.Fatal") + } + + // Equal and by default Error, not Fatal, is called + m = &mockT{} + d = deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{T: m})) + d.Equal("foo", "bar") + if m.calledHelper == false { + t.Errorf("did not call T.Helper") + } + if m.calledError != true { + t.Errorf("did not call T.Error") + } + if m.calledFatal == true { + t.Errorf("called T.Fatal") + } + + // Equal and Fatal + m = &mockT{} + d = deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{T: m, Fatal: true})) + d.Equal("foo", "bar") + if m.calledHelper == false { + t.Errorf("did not call T.Helper") + } + if m.calledError == true { + t.Errorf("called T.Error") + } + if m.calledFatal != true { + t.Errorf("did not call T.Fatal") + } + + // Convenience wrapper + m = &mockT{} + d = deep.Testing(m) + d.Equal("foo", "bar") + if m.calledHelper == false { + t.Errorf("did not call T.Helper") + } + + // Test DiffFormatter and verify that literal "%" in the diff string stay + // literal by being replaced with %% in Errorf/Fatalf format + m = &mockT{} + d = deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{ + T: m, + DiffFormatter: func(diff deep.Diff) string { + return fmt.Sprintf("<%s>", diff) // <-- + }, + })) + d.Equalf("%d", "1%s", "foo %d: {diff}", 9) + errfmt := `foo %d: <[%%d != 1%%s]>` // %% = literal % in Errorf/Fatalf + if m.errFormat != errfmt { + t.Errorf("got error format '%s', expected '%s'", m.errFormat, errfmt) + } + if len(m.errArgs) != 1 || m.errArgs[0].(int) != 9 { + t.Errorf("got erorr args %v, expected [9]", m.errArgs) + } + + // Equalf should work without DiffPlaceholder {diff} + m = &mockT{} + d = deep.NewDiffer(deep.WithInlineTesting(deep.InlineTesting{ + T: m, + })) + errfmt = `goodbye, diff` + d.Equalf("a", "b", errfmt) + if m.errFormat != errfmt { + t.Errorf("got error format '%s', expected '%s'", m.errFormat, errfmt) + } + if len(m.errArgs) != 0 { + t.Errorf("got error args, expected none: %v", m.errArgs) + } +} diff --git a/test/coverage b/test/coverage index e328944..ae5376c 100755 --- a/test/coverage +++ b/test/coverage @@ -1,3 +1,5 @@ #!/bin/sh +export GTK_MODULES="" # avoid Ubuntu/Firefox warning go test -coverprofile=coverage.out "$@" -go tool cover -html=coverage.out +go tool cover -html=coverage.out -o coverage.html +open coverage.html