Skip to content

Commit b665467

Browse files
committed
chore: add identity CID parse tests
1 parent 2662e8f commit b665467

File tree

4 files changed

+280
-4
lines changed

4 files changed

+280
-4
lines changed

impl/graphsync_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/ipld/go-ipld-prime/node/basicnode"
3737
"github.com/ipld/go-ipld-prime/traversal/selector"
3838
"github.com/ipld/go-ipld-prime/traversal/selector/builder"
39+
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
3940
"github.com/libp2p/go-libp2p/core/host"
4041
"github.com/libp2p/go-libp2p/core/peer"
4142
"github.com/libp2p/go-libp2p/core/protocol"
@@ -324,6 +325,47 @@ func TestGraphsyncRoundTrip(t *testing.T) {
324325
}
325326
}
326327

328+
func TestGraphsyncIdentityCIDRoundTrip(t *testing.T) {
329+
// create network
330+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
331+
defer cancel()
332+
td := newGsTestData(ctx, t)
333+
334+
// initialize graphsync on first node to make requests
335+
requestor := td.GraphSyncHost1()
336+
identityDag := testutil.SetupIdentityDAG(ctx, t, td.persistence2)
337+
338+
// initialize graphsync on second node to respond to requests
339+
responder := td.GraphSyncHost2()
340+
assertComplete := assertCompletionFunction(responder, 1)
341+
342+
responder.RegisterIncomingRequestHook(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) {
343+
hookActions.ValidateRequest()
344+
})
345+
346+
finalResponseStatusChan := make(chan graphsync.ResponseStatusCode, 1)
347+
responder.RegisterCompletedResponseListener(func(p peer.ID, request graphsync.RequestData, status graphsync.ResponseStatusCode) {
348+
select {
349+
case finalResponseStatusChan <- status:
350+
default:
351+
}
352+
})
353+
progressChan, errChan := requestor.Request(ctx, td.host2.ID(), identityDag.RootLink, selectorparse.CommonSelector_ExploreAllRecursively)
354+
355+
identityDag.VerifyWholeDAG(ctx, progressChan)
356+
testutil.VerifyEmptyErrors(ctx, t, errChan)
357+
require.Len(t, td.blockStore1, len(identityDag.AllLinks), "did not store all blocks")
358+
359+
// verify listener
360+
var finalResponseStatus graphsync.ResponseStatusCode
361+
testutil.AssertReceive(ctx, t, finalResponseStatusChan, &finalResponseStatus, "should receive status")
362+
require.Equal(t, graphsync.RequestCompletedFull, finalResponseStatus)
363+
364+
drain(requestor)
365+
drain(responder)
366+
assertComplete(ctx, t)
367+
}
368+
327369
func TestGraphsyncRoundTripPartial(t *testing.T) {
328370

329371
// create network

ipldutil/traverser_test.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import (
1111

1212
blocks "github.com/ipfs/go-block-format"
1313
"github.com/ipld/go-ipld-prime"
14+
"github.com/ipld/go-ipld-prime/datamodel"
15+
"github.com/ipld/go-ipld-prime/linking"
1416
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
1517
"github.com/ipld/go-ipld-prime/node/basicnode"
1618
"github.com/ipld/go-ipld-prime/traversal"
1719
"github.com/ipld/go-ipld-prime/traversal/selector"
1820
"github.com/ipld/go-ipld-prime/traversal/selector/builder"
21+
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
1922
"github.com/stretchr/testify/require"
2023

2124
graphsync "github.com/filecoin-project/boost-graphsync"
@@ -165,16 +168,50 @@ func TestTraverser(t *testing.T) {
165168
_, _ = traverser.CurrentRequest()
166169
}
167170
})
171+
172+
t.Run("traverses correctly, DAG with identity CID in the middle", func(t *testing.T) {
173+
store := make(map[ipld.Link][]byte)
174+
persistence := testutil.NewTestStore(store)
175+
identityDag := testutil.SetupIdentityDAG(ctx, t, persistence)
176+
inProgressChan := make(chan graphsync.ResponseProgress)
177+
done := make(chan struct{})
178+
traverser := TraversalBuilder{
179+
Root: identityDag.RootLink,
180+
Selector: selectorparse.CommonSelector_ExploreAllRecursively,
181+
Chooser: func(l datamodel.Link, lc linking.LinkContext) (datamodel.NodePrototype, error) {
182+
return basicnode.Prototype.Any, nil
183+
},
184+
LinkSystem: persistence,
185+
Visitor: func(tp traversal.Progress, node ipld.Node, r traversal.VisitReason) error {
186+
select {
187+
case <-ctx.Done():
188+
case inProgressChan <- graphsync.ResponseProgress{
189+
Node: node,
190+
Path: tp.Path,
191+
LastBlock: tp.LastBlock,
192+
}:
193+
}
194+
return nil
195+
},
196+
}.Start(ctx)
197+
go func() {
198+
identityDag.VerifyWholeDAG(ctx, inProgressChan)
199+
close(done)
200+
}()
201+
checkTraverseSequence(ctx, t, traverser, identityDag.AllBlocks(), nil)
202+
close(inProgressChan)
203+
testutil.AssertDoesReceive(ctx, t, done, "should have completed verification but did not")
204+
})
168205
}
169206

170207
func checkTraverseSequence(ctx context.Context, t *testing.T, traverser Traverser, expectedBlks []blocks.Block, finalErr error) {
171208
t.Helper()
172-
for _, blk := range expectedBlks {
209+
for ii, blk := range expectedBlks {
173210
isComplete, err := traverser.IsComplete()
174211
require.False(t, isComplete)
175212
require.NoError(t, err)
176213
lnk, _ := traverser.CurrentRequest()
177-
require.Equal(t, lnk.(cidlink.Link).Cid, blk.Cid())
214+
require.Equal(t, lnk.(cidlink.Link).Cid.String(), blk.Cid().String(), fmt.Sprintf("unexpected CID @ block %d", ii))
178215
err = traverser.Advance(bytes.NewBuffer(blk.RawData()))
179216
require.NoError(t, err)
180217
}

testutil/identity.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package testutil
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"testing"
8+
9+
graphsync "github.com/filecoin-project/boost-graphsync"
10+
_ "github.com/ipld/go-ipld-prime/codec/raw"
11+
12+
blocks "github.com/ipfs/go-block-format"
13+
"github.com/ipfs/go-cid"
14+
"github.com/ipld/go-ipld-prime"
15+
"github.com/ipld/go-ipld-prime/codec/dagjson"
16+
"github.com/ipld/go-ipld-prime/datamodel"
17+
"github.com/ipld/go-ipld-prime/fluent/qp"
18+
"github.com/ipld/go-ipld-prime/linking"
19+
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
20+
"github.com/ipld/go-ipld-prime/node/basicnode"
21+
"github.com/multiformats/go-multihash"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
// roughly duplicated in github.com/ipld/go-trustless-utils/testutil
26+
27+
type TestIdentityDAG struct {
28+
t testing.TB
29+
loader ipld.BlockReadOpener
30+
31+
RootLink ipld.Link
32+
AllLinks []ipld.Link
33+
}
34+
35+
/* ugly, but it makes a DAG with paths that look like this but doesn't involved dag-pb or unixfs */
36+
var identityDagPaths = []string{
37+
"",
38+
"a/!foo",
39+
"a/b/!bar",
40+
"a/b/c/!baz/identity jump",
41+
"a/b/c/!baz/identity jump/these are my children/blip",
42+
"a/b/c/!baz/identity jump/these are my children/bloop",
43+
"a/b/c/!baz/identity jump/these are my children/bloop/ leaf ",
44+
"a/b/c/!baz/identity jump/these are my children/blop",
45+
"a/b/c/!baz/identity jump/these are my children/leaf",
46+
"a/b/c/d/!leaf",
47+
}
48+
49+
func SetupIdentityDAG(
50+
ctx context.Context,
51+
t testing.TB,
52+
lsys ipld.LinkSystem) *TestIdentityDAG {
53+
54+
allLinks := make([]ipld.Link, 0)
55+
store := func(lp datamodel.LinkPrototype, n datamodel.Node) datamodel.Link {
56+
l, err := lsys.Store(linking.LinkContext{}, lp, n)
57+
require.NoError(t, err)
58+
allLinks = append(allLinks, l)
59+
return l
60+
}
61+
62+
rootLeaf := store(rawlp, basicnode.NewBytes([]byte("leaf node in the root")))
63+
bazLeaf := store(rawlp, basicnode.NewBytes([]byte("leaf node in baz")))
64+
blop := store(djlp, must(qp.BuildList(basicnode.Prototype.Any, -1, func(la datamodel.ListAssembler) {
65+
qp.ListEntry(la, qp.Int(100))
66+
qp.ListEntry(la, qp.Int(200))
67+
qp.ListEntry(la, qp.Int(300))
68+
}))(t))
69+
bloopLeaf := store(rawlp, basicnode.NewBytes([]byte("leaf node in bloop")))
70+
bloop := store(djlp, must(qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) {
71+
qp.MapEntry(ma, "desc", qp.List(-1, func(la datamodel.ListAssembler) {
72+
qp.ListEntry(la, qp.String("this"))
73+
qp.ListEntry(la, qp.String("is"))
74+
qp.ListEntry(la, qp.String("bloop"))
75+
}))
76+
qp.MapEntry(ma, " leaf ", qp.Link(bloopLeaf))
77+
}))(t))
78+
blip := store(djlp, basicnode.NewString("blip!"))
79+
baz := store(djlp, must(qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) {
80+
qp.MapEntry(ma, "desc", qp.List(-1, func(la datamodel.ListAssembler) {
81+
qp.ListEntry(la, qp.String("this"))
82+
qp.ListEntry(la, qp.String("is"))
83+
qp.ListEntry(la, qp.String("baz"))
84+
}))
85+
qp.MapEntry(ma, "these are my children", qp.Map(-1, func(ma datamodel.MapAssembler) {
86+
qp.MapEntry(ma, "blip", qp.Link(blip))
87+
qp.MapEntry(ma, "bloop", qp.Link(bloop))
88+
qp.MapEntry(ma, "blop", qp.Link(blop))
89+
qp.MapEntry(ma, "leaf", qp.Link(bazLeaf))
90+
}))
91+
}))(t))
92+
var bazIdent datamodel.Link
93+
{
94+
// not stored, shouldn't count as a block
95+
ident := must(qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) {
96+
qp.MapEntry(ma, "identity jump", qp.Link(baz))
97+
}))(t)
98+
identBytes := must(ipld.Encode(ident, dagjson.Encode))(t)
99+
mh := must(multihash.Sum(identBytes, multihash.IDENTITY, len(identBytes)))(t)
100+
bazIdent = cidlink.Link{Cid: cid.NewCidV1(cid.DagJSON, mh)}
101+
}
102+
bar := store(djlp, basicnode.NewInt(2020202020202020))
103+
foo := store(djlp, basicnode.NewInt(1010101010101010))
104+
root := store(djlp, must(qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) {
105+
qp.MapEntry(ma, "a", qp.Map(-1, func(ma datamodel.MapAssembler) {
106+
qp.MapEntry(ma, "b", qp.Map(-1, func(ma datamodel.MapAssembler) {
107+
qp.MapEntry(ma, "c", qp.Map(-1, func(ma datamodel.MapAssembler) {
108+
qp.MapEntry(ma, "d", qp.Map(-1, func(ma datamodel.MapAssembler) {
109+
qp.MapEntry(ma, "!leaf", qp.Link(rootLeaf))
110+
}))
111+
qp.MapEntry(ma, "!baz", qp.Link(bazIdent))
112+
}))
113+
qp.MapEntry(ma, "!bar", qp.Link(bar))
114+
}))
115+
qp.MapEntry(ma, "!foo", qp.Link(foo))
116+
}))
117+
}))(t))
118+
119+
return &TestIdentityDAG{
120+
t: t,
121+
loader: lsys.StorageReadOpener,
122+
RootLink: root,
123+
AllLinks: reverse(allLinks), // TODO: slices.Reverse post 1.21
124+
}
125+
}
126+
127+
func (tid *TestIdentityDAG) AllBlocks() []blocks.Block {
128+
blks := make([]blocks.Block, len(tid.AllLinks))
129+
for ii, link := range tid.AllLinks {
130+
reader, err := tid.loader(ipld.LinkContext{}, link)
131+
require.NoError(tid.t, err)
132+
data, err := io.ReadAll(reader)
133+
require.NoError(tid.t, err)
134+
blk, err := blocks.NewBlockWithCid(data, link.(cidlink.Link).Cid)
135+
require.NoError(tid.t, err)
136+
blks[ii] = blk
137+
}
138+
return blks
139+
}
140+
141+
// VerifyWholeDAGWithTypes verifies the given response channel returns the expected responses for the whole DAG
142+
// and that the types in the response are the expected types for the DAG
143+
func (tid *TestIdentityDAG) VerifyWholeDAG(ctx context.Context, responseChan <-chan graphsync.ResponseProgress) {
144+
responses := CollectResponses(ctx, tid.t, responseChan)
145+
tid.checkResponses(responses)
146+
}
147+
148+
func (tid *TestIdentityDAG) checkResponses(responses []graphsync.ResponseProgress) {
149+
var pathIndex int
150+
for ii, response := range responses {
151+
// only check the paths that have links, assume the rest are just describing
152+
// the non-link nodes of the DAG
153+
if response.Path.String() == identityDagPaths[pathIndex] {
154+
if response.LastBlock.Link != nil {
155+
expectedLink := tid.AllLinks[pathIndex]
156+
require.Equal(tid.t, expectedLink.String(), response.LastBlock.Link.String(), fmt.Sprintf("response %d has correct link (%d)", ii, pathIndex))
157+
}
158+
pathIndex++
159+
}
160+
}
161+
require.Equal(tid.t, len(identityDagPaths), pathIndex, "traverses all nodes")
162+
}
163+
164+
func must[T any](v T, err error) func(t testing.TB) T {
165+
return func(t testing.TB) T {
166+
t.Helper()
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
return v
171+
}
172+
}
173+
174+
var djlp = cidlink.LinkPrototype{
175+
Prefix: cid.Prefix{
176+
Version: 1,
177+
Codec: cid.DagJSON,
178+
MhType: multihash.SHA2_256,
179+
MhLength: 32,
180+
},
181+
}
182+
183+
var rawlp = cidlink.LinkPrototype{
184+
Prefix: cid.Prefix{
185+
Version: 1,
186+
Codec: cid.Raw,
187+
MhType: multihash.SHA2_256,
188+
MhLength: 32,
189+
},
190+
}
191+
192+
func reverse[S ~[]E, E any](s S) S {
193+
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
194+
s[i], s[j] = s[j], s[i]
195+
}
196+
return s
197+
}

testutil/testutil.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,11 @@ func VerifyEmptyErrors(ctx context.Context, t testing.TB, errChan <-chan error)
209209
t.Helper()
210210
for {
211211
select {
212-
case _, ok := <-errChan:
212+
case err, ok := <-errChan:
213213
if !ok {
214214
return
215215
}
216-
t.Fatal("errors were sent but shouldn't have been")
216+
t.Fatalf("errors were sent but shouldn't have been: %s", err)
217217
case <-ctx.Done():
218218
t.Fatal("errors channel never closed")
219219
}

0 commit comments

Comments
 (0)