@cryptotaxi247 / kubo / commits / 676e557da

test: port peering test from sharness to Go

This is the slowest test in the sharness test suite, because it has very long sleeps. It usually takes 2+ minutes to run. This new impl runs all peering tests in about 20 seconds, since it polls for conditions instead of sleeping, and runs the tests in parallel. This also has an additional test case for a peer that was never online and then connects.

Gus Eggert committed Dec 16, 2022 at 06:55 UTC 676e557daf45f3b3244931f4568ce0582396db0c
7 files changed +358 -145
test/cli/harness/harness.go
+21
@@ -11,6 +11,8 @@ import (
11
12 logging "github.com/ipfs/go-log/v2"
13 . "github.com/ipfs/kubo/test/cli/testutils"
14 + "github.com/libp2p/go-libp2p/core/peer"
15 + "github.com/multiformats/go-multiaddr"
16 )
17
18 // Harness tracks state for a test, such as temp dirs and IFPS nodes, and cleans them up after the test.
@@ -188,3 +190,22 @@ func (h *Harness) Cleanup() {
190 log.Panicf("removing temp dir %s: %s", h.Dir, err)
191 }
192 }
193 +
194 +// ExtractPeerID extracts a peer ID from the given multiaddr, and fatals if it does not contain a peer ID.
195 +func (h *Harness) ExtractPeerID(m multiaddr.Multiaddr) peer.ID {
196 + var peerIDStr string
197 + multiaddr.ForEach(m, func(c multiaddr.Component) bool {
198 + if c.Protocol().Code == multiaddr.P_P2P {
199 + peerIDStr = c.Value()
200 + }
201 + return true
202 + })
203 + if peerIDStr == "" {
204 + panic(multiaddr.ErrProtocolNotFound)
205 + }
206 + peerID, err := peer.Decode(peerIDStr)
207 + if err != nil {
208 + panic(err)
209 + }
210 + return peerID
211 +}
test/cli/harness/log.go new
+155
@@ -0,0 +1,155 @@
1 +package harness
2 +
3 +import (
4 + "fmt"
5 + "path/filepath"
6 + "runtime"
7 + "sort"
8 + "strings"
9 + "sync"
10 + "testing"
11 + "time"
12 +)
13 +
14 +type event struct {
15 + timestamp time.Time
16 + msg string
17 +}
18 +
19 +type events []*event
20 +
21 +func (e events) Len() int { return len(e) }
22 +func (e events) Less(i, j int) bool { return e[i].timestamp.Before(e[j].timestamp) }
23 +func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
24 +
25 +// TestLogger is a logger for tests.
26 +// It buffers output and only writes the output if the test fails or output is explicitly turned on.
27 +// The purpose of this logger is to allow Go test to run with the verbose flag without printing logs.
28 +// The verbose flag is useful since it streams test progress, but also printing logs makes the output too verbose.
29 +//
30 +// You can also add prefixes that are prepended to each log message, for extra logging context.
31 +//
32 +// This is implemented as a hierarchy of loggers, with children flushing log entries back to parents.
33 +// This works because t.Cleanup() processes entries in LIFO order, so children always flush first.
34 +//
35 +// Obviously this logger should never be used in production systems.
36 +type TestLogger struct {
37 + parent *TestLogger
38 + children []*TestLogger
39 + prefixes []string
40 + prefixesIface []any
41 + t *testing.T
42 + buf events
43 + m sync.Mutex
44 + logsEnabled bool
45 +}
46 +
47 +func NewTestLogger(t *testing.T) *TestLogger {
48 + l := &TestLogger{t: t, buf: make(events, 0)}
49 + t.Cleanup(l.flush)
50 + return l
51 +}
52 +
53 +func (t *TestLogger) buildPrefix(timestamp time.Time) string {
54 + d := timestamp.Format("2006-01-02T15:04:05.999999")
55 + _, file, lineno, _ := runtime.Caller(2)
56 + file = filepath.Base(file)
57 + caller := fmt.Sprintf("%s:%d", file, lineno)
58 +
59 + if len(t.prefixes) == 0 {
60 + return fmt.Sprintf("%s\t%s\t", d, caller)
61 + }
62 +
63 + prefixes := strings.Join(t.prefixes, ":")
64 + return fmt.Sprintf("%s\t%s\t%s: ", d, caller, prefixes)
65 +}
66 +
67 +func (t *TestLogger) Log(args ...any) {
68 + timestamp := time.Now()
69 + e := t.buildPrefix(timestamp) + fmt.Sprint(args...)
70 + t.add(&event{timestamp: timestamp, msg: e})
71 +}
72 +
73 +func (t *TestLogger) Logf(format string, args ...any) {
74 + timestamp := time.Now()
75 + e := t.buildPrefix(timestamp) + fmt.Sprintf(format, args...)
76 + t.add(&event{timestamp: timestamp, msg: e})
77 +}
78 +
79 +func (t *TestLogger) Fatal(args ...any) {
80 + timestamp := time.Now()
81 + e := t.buildPrefix(timestamp) + fmt.Sprint(append([]any{"fatal: "}, args...)...)
82 + t.add(&event{timestamp: timestamp, msg: e})
83 + t.t.FailNow()
84 +}
85 +
86 +func (t *TestLogger) Fatalf(format string, args ...any) {
87 + timestamp := time.Now()
88 + e := t.buildPrefix(timestamp) + fmt.Sprintf(fmt.Sprintf("fatal: %s", format), args...)
89 + t.add(&event{timestamp: timestamp, msg: e})
90 + t.t.FailNow()
91 +}
92 +
93 +func (t *TestLogger) add(e *event) {
94 + t.m.Lock()
95 + defer t.m.Unlock()
96 + t.buf = append(t.buf, e)
97 +}
98 +
99 +func (t *TestLogger) AddPrefix(prefix string) *TestLogger {
100 + l := &TestLogger{
101 + prefixes: append(t.prefixes, prefix),
102 + prefixesIface: append(t.prefixesIface, prefix),
103 + t: t.t,
104 + parent: t,
105 + logsEnabled: t.logsEnabled,
106 + }
107 + t.m.Lock()
108 + defer t.m.Unlock()
109 +
110 + t.children = append(t.children, l)
111 + t.t.Cleanup(l.flush)
112 +
113 + return l
114 +}
115 +
116 +func (t *TestLogger) EnableLogs() {
117 + t.m.Lock()
118 + defer t.m.Unlock()
119 + t.logsEnabled = true
120 + if t.parent != nil {
121 + if t.parent.logsEnabled {
122 + t.parent.EnableLogs()
123 + }
124 + }
125 + fmt.Printf("enabling %d children\n", len(t.children))
126 + for _, c := range t.children {
127 + if !c.logsEnabled {
128 + c.EnableLogs()
129 + }
130 + }
131 +}
132 +
133 +func (t *TestLogger) flush() {
134 + if t.t.Failed() || t.logsEnabled {
135 + t.m.Lock()
136 + defer t.m.Unlock()
137 + // if this is a child, send the events to the parent
138 + // the root parent will print all the events in sorted order
139 + if t.parent != nil {
140 + for _, e := range t.buf {
141 + t.parent.add(e)
142 + }
143 + } else {
144 + // we're the root, sort all the events and then print them
145 + sort.Sort(t.buf)
146 + fmt.Println()
147 + fmt.Printf("Logs for test %q:\n\n", t.t.Name())
148 + for _, e := range t.buf {
149 + fmt.Println(e.msg)
150 + }
151 + fmt.Println()
152 + }
153 + t.buf = nil
154 + }
155 +}
test/cli/harness/node.go
+23 -2
@@ -453,9 +453,8 @@ func (n *Node) Peers() []multiaddr.Multiaddr {
453 Path: n.IPFSBin,
454 Args: []string{"swarm", "peers"},
455 })
456 - lines := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n")
456 var addrs []multiaddr.Multiaddr
458 - for _, line := range lines {
457 + for _, line := range res.Stdout.Lines() {
458 ma, err := multiaddr.NewMultiaddr(line)
459 if err != nil {
460 panic(err)
@@ -465,6 +464,28 @@ func (n *Node) Peers() []multiaddr.Multiaddr {
464 return addrs
465 }
466
467 +func (n *Node) PeerWith(other *Node) {
468 + n.UpdateConfig(func(cfg *config.Config) {
469 + var addrs []multiaddr.Multiaddr
470 + for _, addrStr := range other.ReadConfig().Addresses.Swarm {
471 + ma, err := multiaddr.NewMultiaddr(addrStr)
472 + if err != nil {
473 + panic(err)
474 + }
475 + addrs = append(addrs, ma)
476 + }
477 +
478 + cfg.Peering.Peers = append(cfg.Peering.Peers, peer.AddrInfo{
479 + ID: other.PeerID(),
480 + Addrs: addrs,
481 + })
482 + })
483 +}
484 +
485 +func (n *Node) Disconnect(other *Node) {
486 + n.IPFS("swarm", "disconnect", "/p2p/"+other.PeerID().String())
487 +}
488 +
489 // GatewayURL waits for the gateway file and then returns its contents or times out.
490 func (n *Node) GatewayURL() string {
491 timer := time.NewTimer(1 * time.Second)
test/cli/harness/nodes.go
+4 -16
@@ -3,6 +3,7 @@ package harness
3 import (
4 "sync"
5
6 + . "github.com/ipfs/kubo/test/cli/testutils"
7 "github.com/multiformats/go-multiaddr"
8 "golang.org/x/sync/errgroup"
9 )
@@ -11,9 +12,7 @@ import (
12 type Nodes []*Node
13
14 func (n Nodes) Init(args ...string) Nodes {
14 - for _, node := range n {
15 - node.Init()
16 - }
15 + ForEachPar(n, func(node *Node) { node.Init(args...) })
16 return n
17 }
18
@@ -59,22 +58,11 @@ func (n Nodes) Connect() Nodes {
58 }
59
60 func (n Nodes) StartDaemons() Nodes {
62 - wg := sync.WaitGroup{}
63 - for _, node := range n {
64 - wg.Add(1)
65 - node := node
66 - go func() {
67 - defer wg.Done()
68 - node.StartDaemon()
69 - }()
70 - }
71 - wg.Wait()
61 + ForEachPar(n, func(node *Node) { node.StartDaemon() })
62 return n
63 }
64
65 func (n Nodes) StopDaemons() Nodes {
76 - for _, node := range n {
77 - node.StopDaemon()
78 - }
66 + ForEachPar(n, func(node *Node) { node.StopDaemon() })
67 return n
68 }
test/cli/peering_test.go new
+141
@@ -0,0 +1,141 @@
1 +package cli
2 +
3 +import (
4 + "fmt"
5 + "math/rand"
6 + "testing"
7 + "time"
8 +
9 + "github.com/ipfs/kubo/config"
10 + "github.com/ipfs/kubo/test/cli/harness"
11 + . "github.com/ipfs/kubo/test/cli/testutils"
12 + "github.com/libp2p/go-libp2p/core/peer"
13 + "github.com/stretchr/testify/assert"
14 +)
15 +
16 +func TestPeering(t *testing.T) {
17 + t.Parallel()
18 +
19 + type peering struct {
20 + from int
21 + to int
22 + }
23 +
24 + newRandPort := func() int {
25 + n := rand.Int()
26 + return 3000 + (n % 1000)
27 + }
28 +
29 + containsPeerID := func(p peer.ID, peers []peer.ID) bool {
30 + for _, peerID := range peers {
31 + if p == peerID {
32 + return true
33 + }
34 + }
35 + return false
36 + }
37 +
38 + assertPeered := func(h *harness.Harness, from *harness.Node, to *harness.Node) {
39 + assert.Eventuallyf(t, func() bool {
40 + fromPeers := from.Peers()
41 + if len(fromPeers) == 0 {
42 + return false
43 + }
44 + var fromPeerIDs []peer.ID
45 + for _, p := range fromPeers {
46 + fromPeerIDs = append(fromPeerIDs, h.ExtractPeerID(p))
47 + }
48 + return containsPeerID(to.PeerID(), fromPeerIDs)
49 + }, 20*time.Second, 10*time.Millisecond, "%d -> %d not peered", from.ID, to.ID)
50 + }
51 +
52 + assertNotPeered := func(h *harness.Harness, from *harness.Node, to *harness.Node) {
53 + assert.Eventuallyf(t, func() bool {
54 + fromPeers := from.Peers()
55 + if len(fromPeers) == 0 {
56 + return false
57 + }
58 + var fromPeerIDs []peer.ID
59 + for _, p := range fromPeers {
60 + fromPeerIDs = append(fromPeerIDs, h.ExtractPeerID(p))
61 + }
62 + return !containsPeerID(to.PeerID(), fromPeerIDs)
63 + }, 20*time.Second, 10*time.Millisecond, "%d -> %d peered", from.ID, to.ID)
64 + }
65 +
66 + assertPeerings := func(h *harness.Harness, nodes []*harness.Node, peerings []peering) {
67 + ForEachPar(peerings, func(peering peering) {
68 + assertPeered(h, nodes[peering.from], nodes[peering.to])
69 + })
70 + }
71 +
72 + createNodes := func(t *testing.T, n int, peerings []peering) (*harness.Harness, harness.Nodes) {
73 + h := harness.NewT(t)
74 + nodes := h.NewNodes(n).Init()
75 + nodes.ForEachPar(func(node *harness.Node) {
76 + node.UpdateConfig(func(cfg *config.Config) {
77 + cfg.Routing.Type = config.NewOptionalString("none")
78 + cfg.Addresses.Swarm = []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", newRandPort())}
79 + })
80 +
81 + })
82 +
83 + for _, peering := range peerings {
84 + nodes[peering.from].PeerWith(nodes[peering.to])
85 + }
86 +
87 + return h, nodes
88 + }
89 +
90 + t.Run("bidirectional peering should work (simultaneous connect)", func(t *testing.T) {
91 + t.Parallel()
92 + peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}}
93 + h, nodes := createNodes(t, 3, peerings)
94 +
95 + nodes.StartDaemons()
96 + assertPeerings(h, nodes, peerings)
97 +
98 + nodes[0].Disconnect(nodes[1])
99 + assertPeerings(h, nodes, peerings)
100 + })
101 +
102 + t.Run("1 should reconnect to 2 when 2 disconnects from 1", func(t *testing.T) {
103 + t.Parallel()
104 + peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}}
105 + h, nodes := createNodes(t, 3, peerings)
106 +
107 + nodes.StartDaemons()
108 + assertPeerings(h, nodes, peerings)
109 +
110 + nodes[2].Disconnect(nodes[1])
111 + assertPeerings(h, nodes, peerings)
112 + })
113 +
114 + t.Run("1 will peer with 2 when it comes online", func(t *testing.T) {
115 + t.Parallel()
116 + peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}}
117 + h, nodes := createNodes(t, 3, peerings)
118 +
119 + nodes[0].StartDaemon()
120 + nodes[1].StartDaemon()
121 + assertPeerings(h, nodes, []peering{{from: 0, to: 1}, {from: 1, to: 0}})
122 +
123 + nodes[2].StartDaemon()
124 + assertPeerings(h, nodes, peerings)
125 + })
126 +
127 + t.Run("1 will re-peer with 2 when it disconnects and then comes back online", func(t *testing.T) {
128 + t.Parallel()
129 + peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}}
130 + h, nodes := createNodes(t, 3, peerings)
131 +
132 + nodes.StartDaemons()
133 + assertPeerings(h, nodes, peerings)
134 +
135 + nodes[2].StopDaemon()
136 + assertNotPeered(h, nodes[1], nodes[2])
137 +
138 + nodes[2].StartDaemon()
139 + assertPeerings(h, nodes, peerings)
140 + })
141 +}
test/cli/testutils/strings.go
+14
@@ -7,6 +7,7 @@ import (
7 "net/netip"
8 "net/url"
9 "strings"
10 + "sync"
11
12 "github.com/multiformats/go-multiaddr"
13 manet "github.com/multiformats/go-multiaddr/net"
@@ -75,3 +76,16 @@ func URLStrToMultiaddr(u string) multiaddr.Multiaddr {
76 }
77 return ma
78 }
79 +
80 +// ForEachPar invokes f in a new goroutine for each element of s and waits for all to complete.
81 +func ForEachPar[T any](s []T, f func(T)) {
82 + wg := sync.WaitGroup{}
83 + wg.Add(len(s))
84 + for _, x := range s {
85 + go func(x T) {
86 + defer wg.Done()
87 + f(x)
88 + }(x)
89 + }
90 + wg.Wait()
91 +}
test/sharness/t0171-peering.sh deleted
-127
@@ -1,127 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -test_description="Test peering service"
4 -
5 -. lib/test-lib.sh
6 -
7 -NUM_NODES=3
8 -
9 -test_expect_success 'init iptb' '
10 - rm -rf .iptb/ &&
11 - iptb testbed create -type localipfs -count $NUM_NODES -init
12 -'
13 -
14 -test_expect_success 'disabling routing' '
15 - iptb run -- ipfs config Routing.Type none
16 -'
17 -
18 -for i in $(seq 0 2); do
19 - ADDR="$(printf '["/ip4/127.0.0.1/tcp/%s"]' "$(( 3000 + ( RANDOM % 1000 ) ))")"
20 - test_expect_success "configuring node $i to listen on $ADDR" '
21 - ipfsi "$i" config --json Addresses.Swarm "$ADDR"
22 - '
23 -done
24 -
25 -peer_id() {
26 - ipfsi "$1" config Identity.PeerID
27 -}
28 -
29 -peer_addrs() {
30 - ipfsi "$1" config Addresses.Swarm
31 -}
32 -
33 -peer() {
34 - PEER1="$1" &&
35 - PEER2="$2" &&
36 - PEER_LIST="$(ipfsi "$PEER1" config Peering.Peers || true)" &&
37 - { [[ "$PEER_LIST" == "null" ]] || PEER_LIST_INNER="${PEER_LIST:1:-1}"; } &&
38 - ADDR_INFO="$(printf '[%s{"ID": "%s", "Addrs": %s}]' \
39 - "${PEER_LIST_INNER:+${PEER_LIST_INNER},}" \
40 - "$(peer_id "$PEER2")" \
41 - "$(peer_addrs "$PEER2")")" &&
42 - ipfsi "$PEER1" config --json Peering.Peers "${ADDR_INFO}"
43 -}
44 -
45 -# Peer:
46 -# - 0 <-> 1
47 -# - 1 -> 2
48 -test_expect_success 'configure peering' '
49 - peer 0 1 &&
50 - peer 1 0 &&
51 - peer 1 2
52 -'
53 -
54 -list_peers() {
55 - ipfsi "$1" swarm peers | sed 's|.*/p2p/\([^/]*\)$|\1|' | sort -u
56 -}
57 -
58 -check_peers() {
59 - sleep 20 # give it some time to settle.
60 - test_expect_success 'verifying peering for peer 0' '
61 - list_peers 0 > peers_0_actual &&
62 - peer_id 1 > peers_0_expected &&
63 - test_cmp peers_0_expected peers_0_actual
64 - '
65 -
66 - test_expect_success 'verifying peering for peer 1' '
67 - list_peers 1 > peers_1_actual &&
68 - { peer_id 0 && peer_id 2 ; } | sort -u > peers_1_expected &&
69 - test_cmp peers_1_expected peers_1_actual
70 - '
71 -
72 - test_expect_success 'verifying peering for peer 2' '
73 - list_peers 2 > peers_2_actual &&
74 - peer_id 1 > peers_2_expected &&
75 - test_cmp peers_2_expected peers_2_actual
76 - '
77 -}
78 -
79 -test_expect_success 'startup cluster' '
80 - iptb start -wait &&
81 - iptb run -- ipfs log level peering debug
82 -'
83 -
84 -check_peers
85 -
86 -disconnect() {
87 - ipfsi "$1" swarm disconnect "/p2p/$(peer_id "$2")"
88 -}
89 -
90 -# Bidirectional peering shouldn't cause problems (e.g., simultaneous connect
91 -# issues).
92 -test_expect_success 'disconnecting 0->1' '
93 - disconnect 0 1
94 -'
95 -
96 -check_peers
97 -
98 -# 1 should reconnect to 2 when 2 disconnects from 1.
99 -test_expect_success 'disconnecting 2->1' '
100 - disconnect 2 1
101 -'
102 -
103 -check_peers
104 -
105 -# 2 isn't peering. This test ensures that 1 will re-peer with 2 when it comes
106 -# back online.
107 -test_expect_success 'stopping 2' '
108 - iptb stop 2
109 -'
110 -
111 -# Wait to disconnect
112 -sleep 30
113 -
114 -test_expect_success 'starting 2' '
115 - iptb start 2
116 -'
117 -
118 -# Wait for backoff
119 -sleep 30
120 -
121 -check_peers
122 -
123 -test_expect_success "stop testbed" '
124 - iptb stop
125 -'
126 -
127 -test_done