Skip to content

Commit 77538f9

Browse files
tac0turtletac0turtleCopilot
authored
test: add tests (#2372)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview This pr adds test cases and fixes an issue or two <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a method to indicate if the node is running. - **Bug Fixes** - Improved peer listing to ensure a client does not include itself among its connected or discovered peers. - **Tests** - Introduced comprehensive tests for node lifecycle management and client peer/network info retrieval. - **Refactor** - Unified node lifecycle management into a single method with enhanced error handling and logging. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: tac0turtle <you@example.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0533ac5 commit 77538f9

4 files changed

Lines changed: 285 additions & 24 deletions

File tree

node/light.go

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ type LightNode struct {
3131
Store store.Store
3232
rpcServer *http.Server
3333
nodeConfig config.Config
34+
35+
running bool
3436
}
3537

3638
func newLightNode(
@@ -59,8 +61,21 @@ func newLightNode(
5961
return node, nil
6062
}
6163

62-
// OnStart starts the P2P and HeaderSync services
63-
func (ln *LightNode) OnStart(ctx context.Context) error {
64+
// IsRunning returns true if the node is running.
65+
func (ln *LightNode) IsRunning() bool {
66+
return ln.running
67+
}
68+
69+
// Run implements the Service interface.
70+
// It starts all subservices and manages the node's lifecycle.
71+
func (ln *LightNode) Run(parentCtx context.Context) error {
72+
ctx, cancelNode := context.WithCancel(parentCtx)
73+
defer func() {
74+
ln.running = false
75+
cancelNode()
76+
}()
77+
78+
ln.running = true
6479
// Start RPC server
6580
handler, err := rpcserver.NewServiceHandler(ln.Store, ln.P2P)
6681
if err != nil {
@@ -76,44 +91,77 @@ func (ln *LightNode) OnStart(ctx context.Context) error {
7691
}
7792

7893
go func() {
79-
if err := ln.rpcServer.ListenAndServe(); err != http.ErrServerClosed {
94+
ln.Logger.Info("started RPC server", "addr", ln.nodeConfig.RPC.Address)
95+
if err := ln.rpcServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
8096
ln.Logger.Error("RPC server error", "err", err)
8197
}
8298
}()
8399

84-
ln.Logger.Info("Started RPC server", "addr", ln.nodeConfig.RPC.Address)
85-
86100
if err := ln.P2P.Start(ctx); err != nil {
87-
return err
101+
return fmt.Errorf("error while starting P2P client: %w", err)
88102
}
89103

90104
if err := ln.hSyncService.Start(ctx); err != nil {
91105
return fmt.Errorf("error while starting header sync service: %w", err)
92106
}
93107

94-
return nil
95-
}
108+
<-parentCtx.Done()
109+
ln.Logger.Info("context canceled, stopping node")
110+
cancelNode()
96111

97-
// OnStop stops the light node
98-
func (ln *LightNode) OnStop(ctx context.Context) {
99-
ln.Logger.Info("halting light node...")
112+
ln.Logger.Info("halting light node and its sub services...")
100113

101-
// Use a timeout context to ensure shutdown doesn't hang
102114
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
103115
defer cancel()
104116

105-
err := ln.P2P.Close()
106-
err = errors.Join(err, ln.hSyncService.Stop(shutdownCtx))
117+
var multiErr error
118+
119+
// Stop Header Sync Service
120+
err = ln.hSyncService.Stop(shutdownCtx)
121+
if err != nil {
122+
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
123+
multiErr = errors.Join(multiErr, fmt.Errorf("stopping header sync service: %w", err))
124+
} else {
125+
ln.Logger.Debug("header sync service stop context ended", "reason", err)
126+
}
127+
}
107128

129+
// Shutdown RPC Server
108130
if ln.rpcServer != nil {
109-
err = errors.Join(err, ln.rpcServer.Shutdown(shutdownCtx))
131+
err = ln.rpcServer.Shutdown(shutdownCtx)
132+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
133+
multiErr = errors.Join(multiErr, fmt.Errorf("shutting down RPC server: %w", err))
134+
} else {
135+
ln.Logger.Debug("RPC server shutdown context ended", "reason", err)
136+
}
110137
}
111138

112-
err = errors.Join(err, ln.Store.Close())
113-
ln.Logger.Error("errors while stopping node:", "errors", err)
114-
}
139+
// Stop P2P Client
140+
err = ln.P2P.Close()
141+
if err != nil {
142+
multiErr = errors.Join(multiErr, fmt.Errorf("closing P2P client: %w", err))
143+
}
115144

116-
// IsRunning returns true if the node is running.
117-
func (ln *LightNode) IsRunning() bool {
118-
return ln.P2P != nil && ln.hSyncService != nil
145+
if err = ln.Store.Close(); err != nil {
146+
multiErr = errors.Join(multiErr, fmt.Errorf("closing store: %w", err))
147+
} else {
148+
ln.Logger.Debug("store closed")
149+
}
150+
151+
if multiErr != nil {
152+
if unwrapper, ok := multiErr.(interface{ Unwrap() []error }); ok {
153+
for _, err := range unwrapper.Unwrap() {
154+
ln.Logger.Error("error during shutdown", "error", err)
155+
}
156+
} else {
157+
ln.Logger.Error("error during shutdown", "error", multiErr)
158+
}
159+
ln.Logger.Error("error during shutdown", "error", err)
160+
}
161+
162+
if ctx.Err() != nil {
163+
return ctx.Err()
164+
}
165+
166+
return multiErr
119167
}

node/light_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"cosmossdk.io/log"
9+
ds "github.com/ipfs/go-datastore"
10+
ds_sync "github.com/ipfs/go-datastore/sync"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/rollkit/rollkit/pkg/config"
15+
"github.com/rollkit/rollkit/pkg/genesis"
16+
"github.com/rollkit/rollkit/pkg/p2p"
17+
p2p_key "github.com/rollkit/rollkit/pkg/p2p/key"
18+
)
19+
20+
// TestLightNodeLifecycle tests the light node's lifecycle.
21+
func TestLightNodeLifecycle(t *testing.T) {
22+
assert := assert.New(t)
23+
require := require.New(t)
24+
25+
conf := config.Config{
26+
RootDir: t.TempDir(),
27+
P2P: config.P2PConfig{
28+
ListenAddress: "/ip4/127.0.0.1/tcp/0",
29+
},
30+
RPC: config.RPCConfig{
31+
Address: "127.0.0.1:0",
32+
},
33+
}
34+
gen := genesis.Genesis{
35+
ChainID: "test-chain",
36+
}
37+
p2pKey, err := p2p_key.GenerateNodeKey()
38+
require.NoError(err)
39+
40+
logger := log.NewNopLogger()
41+
p2pMetrics := p2p.NopMetrics()
42+
43+
db := ds_sync.MutexWrap(ds.NewMapDatastore())
44+
45+
p2pClient, err := p2p.NewClient(conf, p2pKey, db, logger, p2pMetrics)
46+
require.NoError(err)
47+
48+
ln, err := newLightNode(conf, gen, p2pClient, db, logger)
49+
require.NoError(err)
50+
require.NotNil(ln)
51+
52+
ctx, cancel := context.WithCancel(context.Background())
53+
defer cancel()
54+
55+
runResult := make(chan error, 1)
56+
go func() {
57+
runResult <- ln.Run(ctx)
58+
}()
59+
60+
time.Sleep(200 * time.Millisecond)
61+
62+
cancel()
63+
64+
// Wait for the Run method to return.
65+
select {
66+
case err := <-runResult:
67+
assert.ErrorIs(err, context.Canceled, "Expected context.Canceled error")
68+
case <-time.After(5 * time.Second):
69+
t.Fatal("Node did not stop gracefully within the timeout")
70+
}
71+
}

pkg/p2p/client.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ func (c *Client) Info() (string, string, string, error) {
219219
func (c *Client) PeerIDs() []peer.ID {
220220
peerIDs := make([]peer.ID, 0)
221221
for _, conn := range c.host.Network().Conns() {
222-
peerIDs = append(peerIDs, conn.RemotePeer())
222+
if conn.RemotePeer() != c.host.ID() {
223+
peerIDs = append(peerIDs, conn.RemotePeer())
224+
}
223225
}
224226
return peerIDs
225227
}
@@ -403,8 +405,11 @@ func (c *Client) GetPeers() ([]peer.AddrInfo, error) {
403405
}
404406

405407
var peers []peer.AddrInfo
406-
for peer := range peerCh {
407-
peers = append(peers, peer)
408+
for p := range peerCh {
409+
if p.ID == c.host.ID() {
410+
continue
411+
}
412+
peers = append(peers, p)
408413
}
409414
return peers, nil
410415
}

pkg/p2p/client_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package p2p
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"testing"
@@ -11,6 +12,7 @@ import (
1112
"github.com/ipfs/go-datastore"
1213
dssync "github.com/ipfs/go-datastore/sync"
1314
libp2p "github.com/libp2p/go-libp2p"
15+
"github.com/libp2p/go-libp2p/core/host"
1416
"github.com/libp2p/go-libp2p/core/peer"
1517
"github.com/multiformats/go-multiaddr"
1618
"github.com/stretchr/testify/assert"
@@ -249,3 +251,138 @@ func ClientInitFiles(t *testing.T, tempDir string) {
249251
}
250252

251253
}
254+
255+
// waitForCondition waits for a condition to be true or a timeout to occur.
256+
// It checks the condition immediately, then polls at a regular interval.
257+
func waitForCondition(timeout time.Duration, conditionFunc func() bool) error {
258+
// Check immediately
259+
if conditionFunc() {
260+
return nil
261+
}
262+
263+
// Poll
264+
ticker := time.NewTicker(100 * time.Millisecond) // Adjust poll interval as needed
265+
defer ticker.Stop()
266+
267+
timer := time.NewTimer(timeout)
268+
defer timer.Stop()
269+
270+
for {
271+
select {
272+
case <-ticker.C:
273+
if conditionFunc() {
274+
return nil
275+
}
276+
case <-timer.C:
277+
return fmt.Errorf("timed out after %v waiting for condition", timeout)
278+
}
279+
}
280+
}
281+
282+
func TestClientInfoMethods(t *testing.T) {
283+
require := require.New(t)
284+
assert := assert.New(t)
285+
logger := log.NewTestLogger(t)
286+
287+
tempDir := t.TempDir()
288+
ClientInitFiles(t, tempDir)
289+
conf := config.DefaultConfig
290+
conf.RootDir = tempDir
291+
conf.ChainID = "test-chain"
292+
293+
mn := mocknet.New()
294+
defer mn.Close()
295+
296+
var clients []*Client
297+
var hosts []host.Host
298+
var err error
299+
300+
for i := 0; i < 3; i++ {
301+
nodeKey, e := key.GenerateNodeKey()
302+
require.NoError(e)
303+
h, e := mn.AddPeer(nodeKey.PrivKey, multiaddr.StringCast("/ip4/127.0.0.1/tcp/0"))
304+
require.NoError(e)
305+
c, e := NewClientWithHost(conf, nodeKey, dssync.MutexWrap(datastore.NewMapDatastore()), logger, NopMetrics(), h)
306+
require.NoError(e)
307+
clients = append(clients, c)
308+
hosts = append(hosts, h)
309+
defer c.Close()
310+
}
311+
312+
client0 := clients[0]
313+
client1 := clients[1]
314+
client2 := clients[2]
315+
316+
// Link all peers in the mocknet
317+
err = mn.LinkAll()
318+
require.NoError(err)
319+
err = mn.ConnectAllButSelf()
320+
require.NoError(err)
321+
322+
// Start clients
323+
ctx, cancel := context.WithCancel(context.Background())
324+
defer cancel()
325+
for _, c := range clients {
326+
err = c.Start(ctx)
327+
require.NoError(err)
328+
}
329+
330+
// Wait for connections to establish and DHT to settle
331+
err = waitForCondition(5*time.Second, func() bool {
332+
connectedPeers := client0.Peers()
333+
return len(connectedPeers) == 2
334+
})
335+
336+
require.NoError(err)
337+
t.Run("GetNetworkInfo", func(t *testing.T) {
338+
netInfo, err := client0.GetNetworkInfo()
339+
assert.NoError(err)
340+
assert.Equal(client0.host.ID().String(), netInfo.ID)
341+
assert.Contains(netInfo.ListenAddress[0], hosts[0].Addrs()[0].String()) // Use h0.Addrs()[0].String()
342+
assert.ElementsMatch([]peer.ID{client1.host.ID(), client2.host.ID()}, netInfo.ConnectedPeers)
343+
})
344+
345+
t.Run("GetPeers", func(t *testing.T) {
346+
peers, err := client0.GetPeers()
347+
assert.NoError(err)
348+
expectedPeerIDs := []peer.ID{client1.host.ID(), client2.host.ID()}
349+
actualPeerIDs := make([]peer.ID, len(peers))
350+
for i, p := range peers {
351+
actualPeerIDs[i] = p.ID
352+
}
353+
assert.ElementsMatch(expectedPeerIDs, actualPeerIDs)
354+
})
355+
356+
t.Run("Peers", func(t *testing.T) {
357+
connectedPeers := client0.Peers()
358+
assert.Len(connectedPeers, 2)
359+
360+
foundClient1 := false
361+
foundClient2 := false
362+
for _, p := range connectedPeers {
363+
if p.NodeInfo.NodeID == client1.host.ID().String() {
364+
foundClient1 = true
365+
} else if p.NodeInfo.NodeID == client2.host.ID().String() {
366+
foundClient2 = true
367+
}
368+
assert.Equal(client0.conf.ListenAddress, p.NodeInfo.ListenAddr)
369+
assert.Equal(client0.chainID, p.NodeInfo.Network)
370+
assert.NotEmpty(p.RemoteIP)
371+
}
372+
assert.True(foundClient1, "client1 not found in connected peers")
373+
assert.True(foundClient2, "client2 not found in connected peers")
374+
})
375+
376+
t.Run("PeerIDs", func(t *testing.T) {
377+
peerIDs := client0.PeerIDs()
378+
assert.ElementsMatch([]peer.ID{client1.host.ID(), client2.host.ID()}, peerIDs)
379+
})
380+
381+
t.Run("Info", func(t *testing.T) {
382+
nodeID, listenAddr, chainID, err := client0.Info()
383+
assert.NoError(err)
384+
assert.NotEmpty(nodeID)
385+
assert.Equal(client0.conf.ListenAddress, listenAddr)
386+
assert.Equal(client0.chainID, chainID)
387+
})
388+
}

0 commit comments

Comments
 (0)