Skip to content

Commit

Permalink
add DeepEqual and start using it in tests
Browse files Browse the repository at this point in the history
The funciton is carefully documented via godoc, so I'm not going to
attempt to document it here again. But as a high-level summary, it's
like a reflect.DeepEqual applied to the ipld.Node interface rather than
reflect.Value.

The only other two noteworthy details are that errors are treated as
panics, and Links are compared directly via ==.

Finally, we add table-driven tests to ensure all edge cases work.

Fixes #173.
  • Loading branch information
mvdan committed May 25, 2021
1 parent b39b96a commit 6d29b3c
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 14 deletions.
153 changes: 153 additions & 0 deletions equal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package ipld

// DeepEqual reports whether x and y are "deeply equal" as IPLD nodes.
// This is similar to reflect.DeepEqual, but based around the Node interface.
//
// Two nodes must have the same kind to be deeply equal.
// If either node has the invalid kind, the nodes are not deeply equal.
//
// Two nodes of scalar kinds (null, bool, int, float, string, bytes, link)
// are deeply equal if their Go values, as returned by AsKind methods, are equal as
// per Go's == comparison operator.
//
// Note that Links are compared in a shallow way, without being followed.
// This will generally be enough, as it's rare to have two different links to the
// same IPLD data by using a different codec or multihash type.
//
// Two nodes of recursive kinds (map, list)
// must have the same length to be deeply equal.
// Their elements, as reported by iterators, must be deeply equal.
// The elements are compared in the iterator's order,
// meaning two maps sorting the same keys differently might not be equal.
//
// Note that this function panics if either Node returns an error.
// We only call valid methods for each Kind,
// so an error should only happen if a Node implementation breaks that contract.
// It is generally not recommended to call DeepEqual on ADL nodes.
func DeepEqual(x, y Node) bool {
xk, yk := x.Kind(), y.Kind()
if xk != yk {
return false
}

switch xk {

// Scalar kinds.
case Kind_Null:
return x.IsNull() == y.IsNull()
case Kind_Bool:
xv, err := x.AsBool()
if err != nil {
panic(err)
}
yv, err := y.AsBool()
if err != nil {
panic(err)
}
return xv == yv
case Kind_Int:
xv, err := x.AsInt()
if err != nil {
panic(err)
}
yv, err := y.AsInt()
if err != nil {
panic(err)
}
return xv == yv
case Kind_Float:
xv, err := x.AsFloat()
if err != nil {
panic(err)
}
yv, err := y.AsFloat()
if err != nil {
panic(err)
}
return xv == yv
case Kind_String:
xv, err := x.AsString()
if err != nil {
panic(err)
}
yv, err := y.AsString()
if err != nil {
panic(err)
}
return xv == yv
case Kind_Bytes:
xv, err := x.AsBytes()
if err != nil {
panic(err)
}
yv, err := y.AsBytes()
if err != nil {
panic(err)
}
return string(xv) == string(yv)
case Kind_Link:
xv, err := x.AsLink()
if err != nil {
panic(err)
}
yv, err := y.AsLink()
if err != nil {
panic(err)
}
// Links are just compared via ==.
// This requires the types to exactly match,
// and the values to be equal as per == too.
// This will generally work,
// as ipld-prime assumes link types to be consistent.
return xv == yv

// Recursive kinds.
case Kind_Map:
if x.Length() != y.Length() {
return false
}
xitr := x.MapIterator()
yitr := y.MapIterator()
for !xitr.Done() && !yitr.Done() {
xkey, xval, err := xitr.Next()
if err != nil {
panic(err)
}
ykey, yval, err := yitr.Next()
if err != nil {
panic(err)
}
if !DeepEqual(xkey, ykey) {
return false
}
if !DeepEqual(xval, yval) {
return false
}
}
return true
case Kind_List:
if x.Length() != y.Length() {
return false
}
xitr := x.ListIterator()
yitr := y.ListIterator()
for !xitr.Done() && !yitr.Done() {
_, xval, err := xitr.Next()
if err != nil {
panic(err)
}
_, yval, err := yitr.Next()
if err != nil {
panic(err)
}
if !DeepEqual(xval, yval) {
return false
}
}
return true

// As per the docs, other kinds such as Invalid are not deeply equal.
default:
return false
}
}
147 changes: 147 additions & 0 deletions equal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package ipld_test

import (
"testing"

"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/fluent/qp"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
basic "github.com/ipld/go-ipld-prime/node/basic" // shorter name for the tests
)

var (
globalNode = basic.NewString("global")
globalLink = func() ipld.Link {
someCid, _ := cid.Cast([]byte{1, 85, 0, 5, 0, 1, 2, 3, 4})
return cidlink.Link{Cid: someCid}
}()
globalLink2 = func() ipld.Link {
someCid, _ := cid.Cast([]byte{1, 85, 0, 5, 0, 5, 6, 7, 8})
return cidlink.Link{Cid: someCid}
}()
)

func qpMust(node ipld.Node, err error) ipld.Node {
if err != nil {
panic(err)
}
return node
}

var deepEqualTests = []struct {
name string
left, right ipld.Node
want bool
}{
{"MismatchingKinds", basic.NewBool(true), basic.NewInt(3), false},

{"SameNodeSamePointer", globalNode, globalNode, true},
// Repeated basicnode.New invocations might return different pointers.
{"SameNodeDiffPointer", basic.NewString("same"), basic.NewString("same"), true},

{"SameKindNull", ipld.Null, ipld.Null, true},
{"DiffKindNull", ipld.Null, ipld.Absent, false},
{"SameKindBool", basic.NewBool(true), basic.NewBool(true), true},
{"DiffKindBool", basic.NewBool(true), basic.NewBool(false), false},
{"SameKindInt", basic.NewInt(12), basic.NewInt(12), true},
{"DiffKindInt", basic.NewInt(12), basic.NewInt(15), false},
{"SameKindFloat", basic.NewFloat(1.25), basic.NewFloat(1.25), true},
{"DiffKindFloat", basic.NewFloat(1.25), basic.NewFloat(1.75), false},
{"SameKindString", basic.NewString("foobar"), basic.NewString("foobar"), true},
{"DiffKindString", basic.NewString("foobar"), basic.NewString("baz"), false},
{"SameKindBytes", basic.NewBytes([]byte{5, 2, 3}), basic.NewBytes([]byte{5, 2, 3}), true},
{"DiffKindBytes", basic.NewBytes([]byte{5, 2, 3}), basic.NewBytes([]byte{5, 8, 3}), false},
{"SameKindLink", basic.NewLink(globalLink), basic.NewLink(globalLink), true},
{"DiffKindLink", basic.NewLink(globalLink), basic.NewLink(globalLink2), false},

{
"SameKindList",
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(7))
qp.ListEntry(am, qp.Int(8))
})),
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(7))
qp.ListEntry(am, qp.Int(8))
})),
true,
},
{
"DiffKindList_length",
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(7))
qp.ListEntry(am, qp.Int(8))
})),
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(7))
})),
false,
},
{
"DiffKindList_elems",
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(7))
qp.ListEntry(am, qp.Int(8))
})),
qpMust(qp.BuildList(basic.Prototype.Any, -1, func(am ipld.ListAssembler) {
qp.ListEntry(am, qp.Int(3))
qp.ListEntry(am, qp.Int(2))
})),
false,
},

{
"SameKindMap",
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(7))
qp.MapEntry(am, "bar", qp.Int(8))
})),
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(7))
qp.MapEntry(am, "bar", qp.Int(8))
})),
true,
},
{
"DiffKindMap_length",
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(7))
qp.MapEntry(am, "bar", qp.Int(8))
})),
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(7))
})),
false,
},
{
"DiffKindMap_elems",
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(7))
qp.MapEntry(am, "bar", qp.Int(8))
})),
qpMust(qp.BuildMap(basic.Prototype.Any, -1, func(am ipld.MapAssembler) {
qp.MapEntry(am, "foo", qp.Int(3))
qp.MapEntry(am, "baz", qp.Int(8))
})),
false,
},

// TODO: tests involving different implementations, once bindnode is ready

}

func TestDeepEqual(t *testing.T) {
t.Parallel()
for _, tc := range deepEqualTests {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := ipld.DeepEqual(tc.left, tc.right)
if got != tc.want {
t.Fatalf("DeepEqual got %v, want %v", got, tc.want)
}
})
}
}
6 changes: 3 additions & 3 deletions schema/gen/go/testLists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestListsContainingMaybe(t *testing.T) {
la.AssembleValue().AssignString("1")
la.AssembleValue().AssignString("2")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
t.Run("nullable", func(t *testing.T) {
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestListsContainingMaybe(t *testing.T) {
la.AssembleValue().AssignString("1")
la.AssembleValue().AssignNull()
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
}
Expand Down Expand Up @@ -207,7 +207,7 @@ func TestListsContainingLists(t *testing.T) {
la.AssembleValue().CreateMap(1, func(ma fluent.MapAssembler) { ma.AssembleEntry("encoded").AssignString("32") })
})
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})

Expand Down
8 changes: 4 additions & 4 deletions schema/gen/go/testMaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestMapsContainingMaybe(t *testing.T) {
ma.AssembleEntry("one").AssignString("1")
ma.AssembleEntry("two").AssignString("2")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
t.Run("nullable", func(t *testing.T) {
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestMapsContainingMaybe(t *testing.T) {
ma.AssembleEntry("one").AssignString("1")
ma.AssembleEntry("none").AssignNull()
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
}
Expand Down Expand Up @@ -206,7 +206,7 @@ func TestMapsContainingMaps(t *testing.T) {
})
t.Run("repr-create", func(t *testing.T) {
nr := creation(t, nrp, "encoded")
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
}
Expand Down Expand Up @@ -276,7 +276,7 @@ func TestMapsWithComplexKeys(t *testing.T) {
ma.AssembleEntry("c:d").AssignString("2")
ma.AssembleEntry("e:f").AssignString("3")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
}
6 changes: 3 additions & 3 deletions schema/gen/go/testStructReprStringjoin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestStructReprStringjoin(t *testing.T) {
nr := fluent.MustBuild(nrp, func(na fluent.NodeAssembler) {
na.AssignString("valoo")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})

Expand Down Expand Up @@ -102,7 +102,7 @@ func TestStructReprStringjoin(t *testing.T) {
nr := fluent.MustBuild(nrp, func(na fluent.NodeAssembler) {
na.AssignString("v1:v2")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})

Expand Down Expand Up @@ -139,7 +139,7 @@ func TestStructReprStringjoin(t *testing.T) {
nr := fluent.MustBuild(nrp, func(na fluent.NodeAssembler) {
na.AssignString("v1-v2:v3-v4")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
})
Expand Down
Loading

0 comments on commit 6d29b3c

Please sign in to comment.