-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
7 changed files
with
314 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.