diff --git a/equal.go b/equal.go new file mode 100644 index 0000000000000000000000000000000000000000..c16ce73ea1148ac4a22351cab38a872be0bcba76 --- /dev/null +++ b/equal.go @@ -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 + } +} diff --git a/equal_test.go b/equal_test.go new file mode 100644 index 0000000000000000000000000000000000000000..61ed2b03815d376574877d206c5d0b91f101bf3a --- /dev/null +++ b/equal_test.go @@ -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) + } + }) + } +} diff --git a/schema/gen/go/testLists_test.go b/schema/gen/go/testLists_test.go index f9ff2bc9ef9656c0482181a90bf914aceb612dfe..ed4d48ac8c5640f3055823447eedd2e7d8d6fa24 100644 --- a/schema/gen/go/testLists_test.go +++ b/schema/gen/go/testLists_test.go @@ -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) }) }) diff --git a/schema/gen/go/testMaps_test.go b/schema/gen/go/testMaps_test.go index b24275556726229d805bb35f7db6613927946a83..7c231447fc96fda07d05d6272378b7cbcec49bb5 100644 --- a/schema/gen/go/testMaps_test.go +++ b/schema/gen/go/testMaps_test.go @@ -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) }) }) } diff --git a/schema/gen/go/testStructReprStringjoin_test.go b/schema/gen/go/testStructReprStringjoin_test.go index 4544da9bfe7f3ab1e66d6b4aee4c8d836ee80b00..61a9b411f9e2a8cf2568ba38594ac9d40e1dd30b 100644 --- a/schema/gen/go/testStructReprStringjoin_test.go +++ b/schema/gen/go/testStructReprStringjoin_test.go @@ -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) }) }) }) diff --git a/schema/gen/go/testStructReprTuple_test.go b/schema/gen/go/testStructReprTuple_test.go index 93a4fd7f481fd4ab566a06246c1dffcdf6d9f862..c113692693cc5af95bc19633e8b85021ba9dc694 100644 --- a/schema/gen/go/testStructReprTuple_test.go +++ b/schema/gen/go/testStructReprTuple_test.go @@ -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) }) }) }) diff --git a/schema/gen/go/testStruct_test.go b/schema/gen/go/testStruct_test.go index 996e3b058bf8989d7a40732b0dcd9fceb835dbe1..454c60be8c9a2ba4b8f31f062643e8861091e3a5 100644 --- a/schema/gen/go/testStruct_test.go +++ b/schema/gen/go/testStruct_test.go @@ -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) }) }) }