Commit 6d29b3c3 authored by Daniel Martí's avatar Daniel Martí

add DeepEqual and start using it in tests

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.
parent b39b96a0
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
}
}
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)
}
})
}
}
......@@ -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) {
......@@ -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)
})
})
}
......@@ -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)
})
})
......
......@@ -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) {
......@@ -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)
})
})
}
......@@ -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)
})
})
}
......@@ -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)
})
})
}
......@@ -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)
})
})
......@@ -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)
})
})
......@@ -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)
})
})
})
......
......@@ -64,7 +64,7 @@ func TestStructReprTuple(t *testing.T) {
nr := fluent.MustBuildList(nrp, 1, func(la fluent.ListAssembler) {
la.AssembleValue().AssignString("valoo")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
......@@ -104,7 +104,7 @@ func TestStructReprTuple(t *testing.T) {
la.AssembleValue().AssignString("2")
la.AssembleValue().AssignString("3")
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
......@@ -138,7 +138,7 @@ func TestStructReprTuple(t *testing.T) {
la.AssembleValue().AssignString("0")
la.AssembleValue().AssignNull()
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
})
......
......@@ -133,7 +133,7 @@ func TestStructNesting(t *testing.T) {
ma.AssembleEntry("q").AssignString("woo")
})
})
Wish(t, n, ShouldEqual, nr)
Wish(t, ipld.DeepEqual(n, nr), ShouldEqual, true)
})
})
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment