go-test / deep Goto Github PK
View Code? Open in Web Editor NEWGolang deep variable equality test that returns human-readable differences
License: MIT License
Golang deep variable equality test that returns human-readable differences
License: MIT License
deep.Equal does behave differently from reflect.DeepEqual when comparing functions.
The documentation of reflect.DeepEqual states for functions:
Func values are deeply equal if both are nil; otherwise they are not deeply equal.
Example Code:
func testFunc(i int) int {
return i
}
func TestEqualForFunctions() {
type TestStruct struct {
Function func(int) int
}
t1 := TestStruct{
Function: testFunc,
}
t2 := TestStruct{
Function: testFunc,
}
fmt.Println("reflect", reflect.DeepEqual(t1, t2))
fmt.Println("deep", deep.Equal(t1, t2))
t1.Function = nil
fmt.Println("reflect", reflect.DeepEqual(t1, t2))
fmt.Println("deep", deep.Equal(t1, t2))
t1.Function = nil
t2.Function = nil
fmt.Println("reflect", reflect.DeepEqual(t1, t2))
fmt.Println("deep", deep.Equal(t1, t2))
}
Output:
reflect false
deep [] // should state that both functions are != nil
reflect false
deep [] // should state that function 1 is not nil but function 2 is nil
reflect true
deep []
It would be great to have option to ignore order of items in slices/maps and just assert that it contains the same element.
Since it does more that equality checking, name could reflect it.
#33 broke the comparison of structs with errors in an unexported fields. THis code used to work just fine with deep 1.0.4, but panics with 1.0.7:
package main
import (
"fmt"
"github.com/go-test/deep"
)
type foo struct {
bar error
}
func main() {
deep.CompareUnexportedFields = true
e1 := foo{bar: fmt.Errorf("error")}
e2 := foo{bar: fmt.Errorf("error")}
deep.Equal(e1, e2)
}
The panic:
$ go run deep_panic.go
panic: reflect: reflect.flag.mustBeExported using value obtained using unexported field
goroutine 1 [running]:
reflect.flag.mustBeExportedSlow(0x2b3)
/usr/local/Cellar/go/1.14.3/libexec/src/reflect/value.go:225 +0x12e
reflect.flag.mustBeExported(...)
/usr/local/Cellar/go/1.14.3/libexec/src/reflect/value.go:216
reflect.Value.Call(0x10e1ca0, 0xc00008e230, 0x2b3, 0x0, 0x0, 0x0, 0xc00008e230, 0x2b3, 0x0)
/usr/local/Cellar/go/1.14.3/libexec/src/reflect/value.go:320 +0x4d
github.com/go-test/deep.(*cmp).equals(0xc00009aeb0, 0x10e1ca0, 0xc00008e230, 0xb4, 0x10e1ca0, 0xc00008e240, 0xb4, 0x1)
/Users/seveas/go/pkg/mod/github.com/go-test/[email protected]/deep.go:145 +0x335b
github.com/go-test/deep.(*cmp).equals(0xc00009aeb0, 0x10e3040, 0xc00008e230, 0x99, 0x10e3040, 0xc00008e240, 0x99, 0x0)
/Users/seveas/go/pkg/mod/github.com/go-test/[email protected]/deep.go:223 +0x246c
github.com/go-test/deep.Equal(0x10e3040, 0xc00008e230, 0x10e3040, 0xc00008e240, 0x0, 0x11203e0, 0xc00008e220)
/Users/seveas/go/pkg/mod/github.com/go-test/[email protected]/deep.go:88 +0x2b4
main.main()
/Users/seveas/code/katyusha/deep_panic.go:16 +0x118
exit status 2
FYI, your repository has been copied here https://github.com/funkygao/golib/blob/master/deep/deep.go
and it doesn't contain your license.
Comparing on a struct with an embedded time.Time causes a panic. Below is a minimal test case to reproduce the panic
package main
import (
"time"
"github.com/go-test/deep"
)
type Time struct {
time.Time
}
func main() {
now := time.Now()
a := Time{now}
b := Time{now}
deep.Equal(a, b)
}
panic: reflect: Call using main.Time as type time.Time
goroutine 1 [running]:
panic(0x4bd8e0, 0xc42000a340)
/usr/lib/go-1.7/src/runtime/panic.go:500 +0x1a1
reflect.Value.call(0x4da060, 0xc42000e2c0, 0x2293, 0x4de107, 0x4, 0xc42003dc90, 0x1, 0x1, 0xc42002a038, 0x13, ...)
/usr/lib/go-1.7/src/reflect/value.go:371 +0x10c0
reflect.Value.Call(0x4da060, 0xc42000e2c0, 0x2293, 0xc42003dc90, 0x1, 0x1, 0xc42000e2c0, 0x2293, 0x0)
/usr/lib/go-1.7/src/reflect/value.go:302 +0xa4
github.com/goraxe/deep_test/vendor/github.com/go-test/deep.(*cmp).equals(0xc42003de50, 0x4da060, 0xc42000e2c0, 0x99, 0x4da060, 0xc42000e2e0, 0x99, 0x0)
/home/goraxe/projects/go/src/github.com/goraxe/deep_test/vendor/github.com/go-test/deep/deep.go:146 +0x2be1
github.com/goraxe/deep_test/vendor/github.com/go-test/deep.Equal(0x4da060, 0xc42000e2c0, 0x4da060, 0xc42000e2e0, 0xc42000e2e0, 0x0, 0x0)
/home/goraxe/projects/go/src/github.com/goraxe/deep_test/vendor/github.com/go-test/deep/deep.go:77 +0x291
main.main()
/home/goraxe/projects/go/src/github.com/goraxe/deep_test/main.go:21 +0x16d
exit status 2
doing a deep equal on errors often panics on reflect calls
&url.Error{ Op: "parse", URL: "http:// invalid.url.com/path?unencoded=true", Err: url.InvalidHostError(" "), }
I have a large struct I am comparing which contains unexported fields and time.Time values.
When comparing this struct, I get the following error:
panic: reflect: reflect.Value.Call using value obtained using unexported field [recovered]
By reading martini-contrib/encoder#9 and some hacky logging, I've narrowed it down to structs that contain time.Time values when using the CompareUnexportedFields
option.
Reproduction code:
package main
import (
"testing"
"time"
"github.com/go-test/deep"
)
func init() {
deep.CompareUnexportedFields = true
}
type Foo struct {
t time.Time
}
func TestFoo(t *testing.T) {
a := &Foo{
t: time.Now(),
}
b := &Foo{
t: time.Now(),
}
if diff := deep.Equal(a, b); diff != nil {
t.Errorf("Diff: \n%v", diff)
}
}
Full stacktrace:
--- FAIL: TestFoo (0.00s)
panic: reflect: reflect.Value.Call using value obtained using unexported field [recovered]
panic: reflect: reflect.Value.Call using value obtained using unexported field
goroutine 6 [running]:
testing.tRunner.func1(0xc4200c00f0)
/usr/local/Cellar/go/1.10/libexec/src/testing/testing.go:742 +0x29d
panic(0x114a060, 0xc420064570)
/usr/local/Cellar/go/1.10/libexec/src/runtime/panic.go:505 +0x229
reflect.flag.mustBeExported(0x22b3)
/usr/local/Cellar/go/1.10/libexec/src/reflect/value.go:218 +0x145
reflect.Value.Call(0x117fe80, 0xc42000a0e0, 0x22b3, 0xc4200cf238, 0x1, 0x1, 0xc42000a0e0, 0x22b3, 0x0)
/usr/local/Cellar/go/1.10/libexec/src/reflect/value.go:307 +0x46
foo/vendor/github.com/go-test/deep.(*cmp).equals(0xc4200cfec8, 0x117fe80, 0xc42000a0e0, 0x1b9, 0x117fe80, 0xc42000a100, 0x1b9, 0x2)
/Users/amu19/go/src/foo/vendor/github.com/go-test/deep/deep.go:147 +0x28ed
foo/vendor/github.com/go-test/deep.(*cmp).equals(0xc4200cfec8, 0x115e3c0, 0xc42000a0e0, 0x199, 0x115e3c0, 0xc42000a100, 0x199, 0x1)
/Users/amu19/go/src/foo/vendor/github.com/go-test/deep/deep.go:184 +0x1598
foo/vendor/github.com/go-test/deep.(*cmp).equals(0xc420060ec8, 0x115e3c0, 0xc42000a0e0, 0x199, 0x1144800, 0xc42000a100, 0x16, 0x0)
/Users/amu19/go/src/foo/vendor/github.com/go-test/deep/deep.go:139 +0x394
foo/vendor/github.com/go-test/deep.Equal(0x1144800, 0xc42000a0e0, 0x1144800, 0xc42000a100, 0x1241681, 0x38, 0x2d2)
/Users/amu19/go/src/foo/vendor/github.com/go-test/deep/deep.go:77 +0x262
foo/internal/model/customer.TestFoo(0xc4200c00f0)
/Users/amu19/go/src/foo/internal/model/customer/customer_test.go:27 +0x11e
testing.tRunner(0xc4200c00f0, 0x118e280)
/usr/local/Cellar/go/1.10/libexec/src/testing/testing.go:777 +0xd0
created by testing.(*T).Run
/usr/local/Cellar/go/1.10/libexec/src/testing/testing.go:824 +0x2e0
Is recursion causing this?
fatal error: concurrent map read and map write
goroutine 213 [running]:
runtime.throw({0x9fc28f, 0x45d4d9})
C:/Program Files/Go/src/runtime/panic.go:1198 +0x71 fp=0xc000546af8 sp=0xc000546ac8 pc=0x4337b1
runtime.mapaccess2(0xc0b963e1a0, 0xc015228750, 0xc0092a2f00)
C:/Program Files/Go/src/runtime/map.go:469 +0x205 fp=0xc000546b38 sp=0xc000546af8 pc=0x40e2e5
reflect.mapaccess(0x943740, 0xc0b963e1a0, 0xc0bfa7db80)
C:/Program Files/Go/src/runtime/map.go:1318 +0x19 fp=0xc000546b60 sp=0xc000546b38 pc=0x45d259
reflect.Value.MapIndex({0x962de0, 0xc009245ad0, 0xc000546e60}, {0x943740, 0xc0b963e1a0, 0x98})
C:/Program Files/Go/src/reflect/value.go:1530 +0xe5 fp=0xc000546bd0 sp=0xc000546b60 pc=0x495aa5
github.com/go-test/deep.(*cmp).equals(0xc000547430, {0x962de0, 0xc0092459e0, 0x95}, {0x962de0, 0xc009245ad0, 0x95}, 0x1)
C:/Users/work/go/pkg/mod/github.com/go-test/[email protected]/deep.go:350 +0x32a8 fp=0xc000546f98 sp=0xc000546bd0 pc=0x8c6a48
github.com/go-test/deep.(*cmp).equals(0xc000547430, {0x9d9ac0, 0xc009245950, 0x99}, {0x9d9ac0, 0xc009245a40, 0x99}, 0x0)
C:/Users/work/go/pkg/mod/github.com/go-test/[email protected]/deep.go:287 +0x2893 fp=0xc000547360 sp=0xc000546f98 pc=0x8c6033
github.com/go-test/deep.EqualNew({0x9d9ac0, 0xc009245950}, {0x9d9ac0, 0xc009245a40})
C:/Users/work/go/pkg/mod/github.com/go-test/[email protected]/deep.go:141 +0x347 fp=0xc0005474a0 sp=0xc000547360 pc=0x8c35c7
`import (
"fmt"
"testing"
"github.com/go-test/deep"
. "github.com/smartystreets/goconvey/convey"
)
func TestStructCompare(t *testing.T) {
type BB struct {
i_BB int
I_BB int
S_BB string
M_BB map[string]string
A_BB []int
}
type AA struct {
i_AA int
I_AA int
S_AA string
M_AA map[string]string
A_AA []int
P_AA *BB
B_AA BB
}
Convey("test struct compare", t, func() {
a1 := AA{}
a2 := AA{}
a1.I_AA = 1
a1.S_AA = "cc"
a1.M_AA = map[string]string{"1": "2"}
a1.A_AA = []int{3, 4}
a1.P_AA = &BB{I_BB: 21, M_BB: map[string]string{"15": "6"}}
a1.B_AA = BB{I_BB: 21, M_BB: map[string]string{"15": "6"}, A_BB: []int{1, 2}}
a2.I_AA = 2
a2.S_AA = "dd"
a2.M_AA = map[string]string{"5": "6"}
a2.A_AA = []int{7, 8}
a2.P_AA = &BB{I_BB: 21, M_BB: map[string]string{"25": "6"}}
a2.B_AA = BB{I_BB: 21, M_BB: map[string]string{"25": "6"}, A_BB: []int{3, 4}}
fmt.Println()
fmt.Printf("%+v \n", a1)
fmt.Printf("%+v \n", a2)
if diff := deep.Equal(a1, a2); diff != nil {
for i, s := range diff {
fmt.Println(i, s)
}
t.Error(diff)
}
})
}`
output:
{i_AA:0 I_AA:1 S_AA:cc M_AA:map[1:2] A_AA:[3 4] P_AA:0x140009aaf00 B_AA:{i_BB:0 I_BB:21 S_BB: M_BB:map[15:6] A_BB:[1 2]}}
{i_AA:0 I_AA:2 S_AA:dd M_AA:map[5:6] A_AA:[7 8] P_AA:0x140009aaf40 B_AA:{i_BB:0 I_BB:21 S_BB: M_BB:map[25:6] A_BB:[3 4]}}
0 I_AA: 1 != 2
1 S_AA: cc != dd
2 M_AA.map[1]: 2 !=
3 M_AA.map[5]: != 6
4 A_AA.slice[0]: 3 != 7
5 A_AA.slice[1]: 4 != 8
6 P_AA.M_BB.map[15]: 6 !=
7 P_AA.M_BB.map[25]: != 6
8 B_AA.M_BB.map[15]: 6 !=
9 B_AA.M_BB.map[25]: != 6
BB.A_BB is diff,but no.
import "encoding/json"
func TestJsonRawMessage(t *testing.T) {
type T json.RawMessage
a := T(`{"foo":1,"bar":2}`)
b := T(`{"bar":2,"foo":1}`)
// just informative begin
aI := map[string]interface{}{}
bI := map[string]interface{}{}
_ = json.Unmarshal(a, &aI)
_ = json.Unmarshal(b, &bI)
fmt.Println(aI)
fmt.Println(bI)
// end
diff := deep.Equal(a, b)
if len(diff) != 0 {
t.Fatalf("expected 0 diff, got %d: %s", len(diff), diff)
}
}
go test -run ^TestJsonRawMessage$ github.com/go-test/deep
map[bar:2 foo:1]
map[bar:2 foo:1]
--- FAIL: TestJsonRawMessage (0.00s)
.../deep/deep_test.go:1603: expected 0 diff, got 8: [slice[2]: 102 != 98 slice[3]: 111 != 97 slice[4]: 111 != 114 slice[7]: 49 != 50 slice[10]: 98 != 102 slice[11]: 97 != 111 slice[12]: 114 != 111 slice[15]: 50 != 49]
FAIL
FAIL github.com/go-test/deep 0.002s
FAIL
might be tricky tho, since the actual type within there can be any json supported type
in commit 929fce9 the check of Equal()
function was moved under the reflect.Struct
case.
and here is an example why this is wrong:
https://play.golang.org/p/AlgXufFrO1g
package main
import (
"fmt"
"net"
"github.com/go-test/deep"
)
func main() {
ipA := net.ParseIP("1.2.3.4")
ipB := net.ParseIP("1.2.3.4").To4()
fmt.Println(deep.Equal(ipA, ipB))
fmt.Println(ipA.Equal(ipB))
}
ipA
and ipB
has different len of their slice inside, but they still represents the same ip. however deep.Equal()
say there is a diff between ipA
and ipB
.
not only structs may have an Equal()
method.
Currently comparing two "github.com/google/uuid" is not really readable since it is checked as array and looks like this:
UUID.array[0]: 93 != 96 UUID.array[1]: 181 != 145 UUID.array[2]: 180 != 187 UUID.array[3]: 80 != 70 UUID.array[4]: 220 != 109 UUID.array[5]: 73 != 90 UUID.array[6]: 73 != 79 UUID.array[7]: 212 != 138 UUID.array[8]: 181 != 149 UUID.array[9]: 244 != 206
or we can get following test:
func TestUUID(t *testing.T) {
uuid1, _ := uuid.Parse("ac0bd5ef-6f92-4b03-953d-a2c4b9828c8a")
uuid2, _ := uuid.Parse("de09e3ce-227f-49a1-b54d-9fe13af54375")
diff := deep.Equal(uuid1, uuid1)
if len(diff) > 0 {
t.Error("should be equal:", diff)
}
diff = deep.Equal(uuid1, uuid2)
if diff == nil {
t.Fatal("no diff")
}
if len(diff) != 10 {
t.Error("uuid expects array of len() == 10:", diff)
}
if diff[0] != "array[0]: 172 != 222" {
t.Error("wrong diff:", diff[0])
}
if diff[1] != "array[1]: 11 != 9" {
t.Error("wrong diff:", diff[1])
}
if diff[2] != "array[2]: 213 != 227" {
t.Error("wrong diff:", diff[2])
}
if diff[3] != "array[3]: 239 != 206" {
t.Error("wrong diff:", diff[3])
}
if diff[4] != "array[4]: 111 != 34" {
t.Error("wrong diff:", diff[4])
}
if diff[5] != "array[5]: 146 != 127" {
t.Error("wrong diff:", diff[5])
}
if diff[6] != "array[6]: 75 != 73" {
t.Error("wrong diff:", diff[6])
}
if diff[7] != "array[7]: 3 != 161" {
t.Error("wrong diff:", diff[7])
}
if diff[8] != "array[8]: 149 != 181" {
t.Error("wrong diff:", diff[8])
}
if diff[9] != "array[9]: 61 != 77" {
t.Error("wrong diff:", diff[9])
}
}
Would it be maybe interesting to implement something like following or is it preferred to not mix other types?
if aType == reflect.TypeOf(uuid.UUID{}) {
aUUID := a.Interface().(uuid.UUID)
bUUID := b.Interface().(uuid.UUID)
if aUUID.String() != bUUID.String() {
c.saveDiff(aUUID.String(), bUUID.String())
}
return
}
switch aKind {
Maybe even more interesting for me would be to have CustomCompare
like below which would allow this lib to be kept "pure".
I am also using something like getUUIDByName("my_cool_uuid_name")
in my tests and this would allow to match uuids back to "my_cool_uuid_name"
for even better readability.
// NilMapsAreEmpty causes a nil map to be equal to an empty map.
NilMapsAreEmpty = false
// CustomCompare allows to implement custom behaviour for specific type
// and should return if type was handled and what is difference between a and b
CustomCompare func(abType reflect.Type, a, b reflect.Value) (
bool, *string, *string) = nil
...
if CustomCompare != nil {
handled, aDiff, bDiff := CustomCompare(aType, a, b)
if handled {
if aDiff != nil || bDiff != nil {
c.saveDiff(getNilOrValue(aDiff), getNilOrValue(bDiff))
}
return
}
}
If I inspect the double linked datastructure described below:
type CLITree struct {
Type argType
Name string
Children []*CLITree
Parent *CLITree
}
For a trivial datastructure I get the following error:
$ go test
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc020160358 stack=[0xc020160000, 0xc040160000]
fatal error: stack overflow
runtime stack:
runtime.throw(0x595c2b, 0xe)
/home/david/opt/go/src/runtime/panic.go:1117 +0x72
runtime.newstack()
/home/david/opt/go/src/runtime/stack.go:1069 +0x7ed
runtime.morestack()
/home/david/opt/go/src/runtime/asm_amd64.s:458 +0x8f
goroutine 18 [running]:
fmt.(*pp).printArg(0xc0000a41a0, 0x5632e0, 0x68b5a0, 0x64)
/home/david/opt/go/src/fmt/print.go:634 +0x9e5 fp=0xc020160368 sp=0xc020160360 pc=0x4ddb85
fmt.(*pp).doPrintf(0xc0000a41a0, 0x594bc5, 0x9, 0xc020160660, 0x1, 0x1)
/home/david/opt/go/src/fmt/print.go:1026 +0x168 fp=0xc020160458 sp=0xc020160368 pc=0x4e0aa8
fmt.Sprintf(0x594bc5, 0x9, 0xc020160660, 0x1, 0x1, 0x0, 0x0)
/home/david/opt/go/src/fmt/print.go:219 +0x66 fp=0xc0201604b0 sp=0xc020160458 pc=0x4d9de6
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x55ffa0, 0xc0000b0758, 0x197, 0x55ffa0, 0xc0000b0658, 0x197, 0xcccc9)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:341 +0x183c fp=0xc020160730 sp=0xc0201604b0 pc=0x54c13c
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x57fb00, 0xc0000b0740, 0x199, 0x57fb00, 0xc0000b0640, 0x199, 0xcccc8)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:223 +0x242d fp=0xc0201609b0 sp=0xc020160730 pc=0x54cd2d
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x57fb00, 0xc0000b0740, 0x199, 0x55c700, 0xc0000b06b0, 0x196, 0xcccc7)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:162 +0x31d9 fp=0xc020160c30 sp=0xc0201609b0 pc=0x54dad9
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x57fb00, 0xc0000b0780, 0x199, 0x57fb00, 0xc0000b0680, 0x199, 0xcccc6)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:223 +0x242d fp=0xc020160eb0 sp=0xc020160c30 pc=0x54cd2d
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x57fb00, 0xc0000b0780, 0x199, 0x55c700, 0xc00009e590, 0x196, 0xcccc5)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:162 +0x31d9 fp=0xc020161130 sp=0xc020160eb0 pc=0x54dad9
github.com/go-test/deep.(*cmp).equals(0xc04015fe50, 0x55ffa0, 0xc0000b0758, 0x197, 0x55ffa0, 0xc0000b0658, 0x197, 0xcccc4)
/home/david/go/pkg/mod/github.com/go-test/[email protected]/deep.go:343 +0x1b78 fp=0xc0201613b0 sp=0xc020161130 pc=0x54c478
...
...additional frames elided...
created by testing.(*T).Run
/home/david/opt/go/src/testing/testing.go:1238 +0x2b3
goroutine 1 [chan receive]:
testing.(*T).Run(0xc000082600, 0x5957ed, 0xd, 0x59e868, 0x489fe6)
/home/david/opt/go/src/testing/testing.go:1239 +0x2da
testing.runTests.func1(0xc000082480)
/home/david/opt/go/src/testing/testing.go:1511 +0x78
testing.tRunner(0xc000082480, 0xc000093de0)
/home/david/opt/go/src/testing/testing.go:1193 +0xef
testing.runTests(0xc0000bc048, 0x693580, 0x1, 0x1, 0xc01514bef2b74a44, 0x8bb2cc5311, 0x69bc40, 0x595925)
/home/david/opt/go/src/testing/testing.go:1509 +0x2fe
testing.(*M).Run(0xc0000f0000, 0x0)
/home/david/opt/go/src/testing/testing.go:1417 +0x1eb
main.main()
_testmain.go:43 +0x138
exit status 2
NOTE: go-spew handles them properly by displaying already shown:
(*getoptions.CLITree)({
Type: (getoptions.argType) 0,
Name: (string) (len=46) "/tmp/go-build309650026/b001/go-getoptions.test",
Children: ([]*getoptions.CLITree) (len=2) {
(*getoptions.CLITree)({
Type: (getoptions.argType) 2,
Name: (string) (len=4) "opt1",
Children: ([]*getoptions.CLITree) {
},
Parent: (*getoptions.CLITree)(<already shown>)
}),
(*getoptions.CLITree)({
Type: (getoptions.argType) 3,
Name: (string) (len=3) "cmd",
Children: ([]*getoptions.CLITree) (len=1) {
(*getoptions.CLITree)({
Type: (getoptions.argType) 2,
Name: (string) (len=4) "opt2",
Children: ([]*getoptions.CLITree) {
},
Parent: (*getoptions.CLITree)(<already shown>)
})
},
Parent: (*getoptions.CLITree)(<already shown>)
})
},
Parent: (*getoptions.CLITree)(<nil>)
})
Hey, thank you for your time, deep is very handy!
I do have a problem tho, I need to be able to ignore certain struct fields, so I cloned the repo and implemented it locally for now, would you like a PR?
It looks something like this: (briefly)
func TestStructWithTags(t *testing.T) {
type s1 struct {
same int
modified int
sameIgnored int `deep:"-"`
modifiedIgnored int `deep:"-"`
ExportedSame int
ExportedModified int
ExportedSameIgnored int `deep:"-"`
ExportedModifiedIgnored int `deep:"-"`
}
type s2 struct {
s1
same int
modified int
sameIgnored int `deep:"-"`
modifiedIgnored int `deep:"-"`
ExportedSame int
ExportedModified int
ExportedSameIgnored int `deep:"-"`
ExportedModifiedIgnored int `deep:"-"`
recurseInline s1
recursePtr *s2
}
sa := s2{[...]} // omitted, "same" fields are equal, "modified" are multiplied by 10
sb := s2{[...]} // omitted, "same" fields are equal, "modified" are multiplied by 10
// with compare unexported = true
want := []string{
"s1.modified: 1 != 10",
"s1.ExportedModified: 5 != 50",
"modified: 1 != 10",
"ExportedModified: 5 != 50",
"recurseInline.modified: 1 != 10",
"recurseInline.ExportedModified: 5 != 50",
"recursePtr.modified: 1 != 10",
"recursePtr.ExportedModified: 5 != 50",
}
}
Cheers
I understand that it doesn't make sense to go in and try to support all possible core-ish types. E.g. like json.RawMessage
as in #62.
Requesting an Equal
method for the parent type may not be the best way to solve the culprit, a few caveats:
Equal
methodEqual
method is actually impossibleWhat I've seen in different other utilities is to offer a Hook for a type, which could be what is just missing. deep
doesn't need to understand the type except to provide the framework for these hooks and this also allows to prototype a compare functions also for foreign types.
Examples:
deep already uses the package reflect
, so there's no additional package needed.
Idea very rough from mind and may be incorrect. Good enough to show the idea:
import "reflect"
// signature of an equal func
type EqualFunc func(a, b reflect.Value) (bool, error)
// holds all the provided custom equal funcs
var customEqualFuncs map[reflect.Type]EqualFunc
// add a custom equal func
func SetEqualFunc(t reflect.Type, f EqualFunc ) {
customEqualFuncs[t] = f
}
// during equal() check whether a custom equal method exists
func equal(a, b interface{}) ... {
...
// ^ has no Equal method, check if a custom equal func exists
t := reflect.TypeOf(a)
if f, exists := customEqualFuncs[t]; exists {
isequal, err := f(reflect.ValueOf(a),reflect.ValueOf(b))
...
return ...
}
}
Would be used like:
deep.SetEqualFunc(reflect.TypeOf(json.RawMessage{}), func ... {
....
return isequal, iserr
}
Missing above: diff return, which would need to be incorporated in the idea as well
Options to explore for EqualFunc
:
type EqualFunc func(a, b interface) (bool, error)
and use actual values instead of reflect.ValueIs such a hook a fitting candidate for deep in your opinion?
This program
package main
import (
"database/sql"
"github.com/go-test/deep"
)
type NullString struct{ sql.NullString }
func main() {
a := map[string]interface{}{
"a": NullString{sql.NullString{"foo", true}},
}
b := map[string]interface{}{
"a": sql.NullString{"foo", true},
}
deep.Equal(b, a)
}
panics with
panic: reflect: Field index out of range
goroutine 1 [running]:
reflect.Value.Field(0x10e1880, 0xc42000a060, 0x99, 0x1, 0x10d1a60, 0xc42000a090, 0x81)
/usr/local/Cellar/go/1.9.2/libexec/src/reflect/value.go:769 +0x14c
github.com/go-test/deep.(*cmp).equals(0xc42003de98, 0x10db460, 0xc42000e2c0, 0x94, 0x10db460, 0xc42000e2d0, 0x94, 0x1)
/Users/mibk/src/github.com/go-test/deep/deep.go:179 +0x648
github.com/go-test/deep.(*cmp).equals(0xc42003de98, 0x10dd240, 0xc4200721e0, 0x15, 0x10dd240, 0xc4200721b0, 0x15, 0x0)
/Users/mibk/src/github.com/go-test/deep/deep.go:225 +0x1810
github.com/go-test/deep.Equal(0x10dd240, 0xc4200721e0, 0x10dd240, 0xc4200721b0, 0xc4200801a8, 0xc42003df70, 0x1004324)
/Users/mibk/src/github.com/go-test/deep/deep.go:77 +0x284
main.main()
/Users/mibk/src/github.com/mibk/junk/deep/main.go:18 +0x211
exit status 2
Note that if I flip the arguments (deep.Equal(a, b)
), the program works just fine.
go version go1.9.2 darwin/amd64
GOARCH="amd64"
GOOS="darwin"
Hi,
would it be possible to add an option "TimePrecision" analog to "FloatPrecision" which would permit to Truncate the times before comparison.
Reason:
I use deep.Equal to test my Database Interface. I compare the stored Struct with the original one (which contains timestamps). The DB I use (postgres) has a time resolution of Microsecond whereas the golang time.Time has a resolution of Nanosecond. Being able to truncate the timestamps would permit to compare those two.
thanks for considering
Is there any reason to generate diff text using
c.saveDiff(a.Int(), b.Int())
instead of
c.saveDiff(a, b)
At a glance second way is better because it keeps original type and thus if that type implements Stringer interface then diff will contains values in much more readable form than just a number. (With a lot of iota
consts reading diff with numbers is really hard.) Of course, same probably apply to other types, not just to integer ones.
Arrays content is not compared, for instance deep.Equal([3]int{1, 2, 3}, [3]int{}) == nil
is true.
Seems that something strange happening when wrapping inside
if len(equal) > 1 {
t.Fatal(spew.Sdump(equal))
}
new fields with errors added which are actually equal
I'm using v1.0.4.
This program:
package main
import (
"fmt"
"github.com/go-test/deep"
v6 "github.com/olivere/elastic"
v7 "github.com/olivere/elastic/v7"
)
func main() {
errv6 := v6.Error{}
errv7 := v7.Error{}
fmt.Printf("diff: %v\n", deep.Equal(errv6, errv7))
}
produces the output diff: [elastic.Error != elastic.Error]
which gives little indication for why there is a difference between errv6
and errv7
. I have used github.com/olivere/elastic
here but the problem is not specific to that module. Perhaps the full import path can be included in the diff if two distinct types have the same name?
Not sure if there is a limitation on the depth with the default package
reflect.DeepEqual(got, tt.want)
but changing the code out with just
if diff := deep.Equal(got, tt.want); diff != nil {
t.Error(diff)
}
is kind of dangerous. The dept default is set to just 10.
I had to set it to
deep.MaxDepth = 100
to find my issue.
Maybe generate a warning when the struct is deeper and you stop checking?
Or increase the default much higher ?
Probably the best is
if !reflect.DeepEqual(got, tt.want) {
//try a friendly deep error message
if diff := deep.Equal(got, tt.want); diff != nil {
t.Error(diff)
} else {
t.Errorf("deepEqual failed \n%v\nbut I want \n%v", got, tt.want)
}
}
As subj states looks like strings in struct fields are not compared:
if diff := deep.Equal(struct{ foo string }{foo: "aa"}, struct{ foo string }{foo: "aab"}); diff != nil {
t.Error(diff)
}
will pass without error
var err1 primKindError = "abc"
var err2 error = fmt.Errorf("abc")
t1 := tWithError{
Error: err1,
}
t2 := tWithError{
Error: err2,
}
Those should be equal, but as of v1.0.5 they're not:
--- FAIL: TestErrorDifferentTypesSameString (0.00s)
deep_test.go:1336: expected zero diffs, got 1: [Error: deep_test.primKindError != *errors.errorString]
because,
if aType.Implements(errorType) && bType.Implements(errorType) {
if (!aElem || !a.IsNil()) && (!bElem || !b.IsNil()) {
aString := a.MethodByName("Error").Call(nil)[0].String()
bString := b.MethodByName("Error").Call(nil)[0].String()
if aString != bString {
c.saveDiff(aString, bString)
return
}
}
}
we don't return from that block if they're equal, we keep comparing which results in the false positive on their different underlying type.
https://play.golang.org/p/KFbA0PmNoxO
as you can see the two buffer do not have the same content
but deep consider it all fine.
The following code snippet now panics. Used to work with v1.0.1 and older
package main
import (
"fmt"
"github.com/go-test/deep"
"github.com/pkg/errors"
)
func main() {
fmt.Println(
deep.Equal(
errors.Wrap(
errors.New("test error"), "wrapper"),
errors.Wrap(
errors.New("test error"), "wrapper"),
),
)
}
Please see the example 5) from this playground: https://go.dev/play/p/M-VZFkXte9T
With FLAG_IGNORE_SLICE_ORDER set, it causes a panic.
It happens at:
func (c *cmp) equals(a, b reflect.Value, level int) {
...
am[a.Index(i).Interface()] += 1
...
}
where the code tries to use the map[string]interface{} as a key when the child is a slice.
When reading the comments in the code:
// FLAG_IGNORE_SLICE_ORDER 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.
This states it is unsupported to use it with this type of data. I was wondering if it is possible to add this feature in the future.
At least this ticket could save others some time when they do a web search for the cause of the error and did not read the documentation ;)
Hi,
Ive started using this library its great!
I have been playing around testing this and works well. We have some big JSON that gets marshalled into structs, ive found changing something fairly deeply nested wasnt being picked up.
So orderLines is a slice, it DOES pick up a change to a field in the top of the slice like Id
e.g OrderEvent.Order.OrderLines.slice[0].Id
but if its another struct within the slice, it doesnt pick this up, like 'Gross' below
e.g OrderEvent.Order.OrderLines.slice[0].UnitPrice.Gross (float64)
Is this a know thing? Is there a limit how deep you can go?
Thanks!
I’m documenting a few edge-case bugs that came up while I was tinkering with the package. I have a branch with fixes for most of the currently open issues as well as the ones below. I just want to have them written down some where just in case I don’t get the PR open, and my code falls into the void.
The weirds:
Equal
. Say func (Type) Equal(Type) int
.==
as true.value.IsValid()
as a definitive dereferenced nil-pointer, but this is not guaranteed. (Fixing this requires checking for the nil-pointers before derefing with .Elem()
. When fixed, this lets us be a little more specific between <invalid value>
and <nil pointer>
)deep.Equal(nil, bType)
is not strictly comparing a <nil pointer>
to a bType, but rather an “untyped nil”, which compares as inequal to a typed nil-pointer. (The bane of everyone who wonders how their return nil
function could possible not compare equal to nil
in the caller)Deep equals doesn't appear to check embedded values:
package main
import (
"fmt"
"github.com/go-test/deep"
)
type sub struct{
subvar string
}
type top struct{
sub
}
func main() {
val1 := top{sub: sub{subvar: "x"}}
val2 := top{sub: sub{subvar: "y"}}
fmt.Println(deep.Equal(val1, val1)) // no diffs, fine
fmt.Println(deep.Equal(val1, val2)) // no diffs, not so fine
}
Instead of // 6 decimal places is close enough
which looks like a bad joke, floating point numbers should be* compared using epsilon, using
|a - b| < epsilon
or
|(b - a)/b| < epsilon
References:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.