Skip to content

Commit e7fe6fd

Browse files
committed
integration/rpctest,debugstream,btcd: improve test harness
integration/rpctest: Fixed a goroutine leak caused by the [memWallet.Start] procedure which was spinning up a goroutine that was never terminated. Also, added a new method [memWallet.Stop], which is called by [Harness.TearDown], sending a stop request to the wallet and waiting until the wallet stops. Also added a test to [TestHarness] to catch goroutine leakage. Changed [New], initializing the [Harness] with an options struct instead of several fields that may be set to the zero value of the type, cleaning up the API and improving extensibility. Previously [Harness.SetUp] was receiving a boolean to generate a test chain and a integer for the required number of mature outputs, but internally, the test chain was being generated only if the number of mature outputs was different than zero and the boolean was true, this means that we can remove the boolean and create the test chain only when the required numbed of mature UTXO's is greater than zero. Add configuration options to [Harness.SetUp] and [Harness.TearDown], enabling future functionality extension without breaking changes and also avoiding the need of passing zero value arguments to get the default config. Add option to [Harness.SetUp] allowing to skip starting RPC and Wallet, avoiding errors being returned in scenarios where the node shuts down before starting the RPC, like when dropping cfindex with --dropcfindex. Add option to [Harness.SetUp] allowing to skip the wait for memwallet to sync up to the node best height, avoiding blocking forever when the node is shut down before the wallet receives all block connected events, because in this case wallet best height would be less than the node best height. Pass btcd args in [Harness.SetUp] instead of [New], enabling restarting the node with different configuration but same state. Make [Harness.TearDown] return error if the btcd process exits with non zero status code, previously the error returned by [exec.Cmd.Wait] was being ignored. This change enables tests to assert if the node stopped successfully or not. NoNodeCleanup option added to [Harness.TearDown], which makes possible to write tests that restart the node while keeping state. NoShutdownSignal option added to [Harness.Teardown], which instead of sending termination signal, blocks until the node process finishes enabling testing scenarios where the node process exits by itself. Added debug handler to [New] options, allowing integration tests to easily process debug events. The btcd executable used by the Harness is now being built with the debug tag, but the debug stream will be started only if the debug handler is also passed to the Harness. debugstream: The package debugstream provides two utility types, [Stream] and [Client]. [Stream] listens on a TCP endpoint and broadcasts events to connected clients. The [Event] type has a uint64 code and a byte slice data fields . The [Stream] implementation is used only when btcd is compiled with the debug tag, otherwise a nop implementation is used. [Client] connects with [Stream] via TCP and starts receiving the events , which are passed into a handler callback, allowing assertions inside tests. All events broadcasted in the stream are stored in memory and sent to all clients, even if the client connects after the broadcast of some events, it still receives all of them. btcd: btcd integrated with debugstream, so that we can send debug events and improve testing capabilities. The hidden flag --debugstream=<host:port> starts the [Stream] when btcd is compiled with the debug tag, otherwise it is a NOP operation, using StreamNOP, which does nothing.
1 parent 6cfd717 commit e7fe6fd

28 files changed

Lines changed: 1343 additions & 223 deletions

btcd.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"github.com/btcsuite/btcd/blockchain/indexers"
2020
"github.com/btcsuite/btcd/database"
21+
"github.com/btcsuite/btcd/debugstream"
2122
"github.com/btcsuite/btcd/limits"
2223
"github.com/btcsuite/btcd/ossec"
2324
)
@@ -56,6 +57,24 @@ func btcdMain(serverChan chan<- *server) error {
5657
}
5758
}()
5859

60+
// DebugStream is enabled only if btcd is compiled with the debug tag.
61+
// Otherwise a nop implementation is used.
62+
debugstream.S = debugstream.New()
63+
if dsListen := cfg.DebugStream; dsListen != "" {
64+
err := debugstream.S.Listen(dsListen)
65+
if err != nil {
66+
return fmt.Errorf("error starting debug stream: %v",
67+
err)
68+
}
69+
debugstream.S.Broadcast(debugstream.Event{
70+
Code: debugstream.DEStart,
71+
})
72+
defer debugstream.S.Shutdown()
73+
defer debugstream.S.Broadcast(debugstream.Event{
74+
Code: debugstream.DEShutdown,
75+
})
76+
}
77+
5978
// Get a channel that will be closed when a shutdown signal has been
6079
// triggered either from an OS signal such as SIGINT (Ctrl+C) or from
6180
// another subsystem such as the RPC server.

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type config struct {
120120
DataDir string `short:"b" long:"datadir" description:"Directory to store data"`
121121
DbType string `long:"dbtype" description:"Database backend to use for the Block Chain"`
122122
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"`
123+
DebugStream string `long:"debugstream" hidden:"true" description:"TCP listen address of the debug stream. To use this feature btcd must also be compiled with debug tag."`
123124
DropAddrIndex bool `long:"dropaddrindex" description:"Deletes the address-based transaction index from the database on start up and then exits."`
124125
DropCfIndex bool `long:"dropcfindex" description:"Deletes the index used for committed filtering (CF) support from the database on start up and then exits."`
125126
DropTxIndex bool `long:"droptxindex" description:"Deletes the hash-based transaction index from the database on start up and then exits."`

debugstream/client.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package debugstream
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"time"
7+
)
8+
9+
// Client calls handler with events coming from the [StreamServer].
10+
type Client struct {
11+
addr string
12+
13+
conn *net.TCPConn
14+
handler func(e Event)
15+
16+
stop chan struct{}
17+
done chan struct{}
18+
}
19+
20+
// NewClient initializes a [Client] instance.
21+
func NewClient(streamAddr string, handler func(ev Event)) *Client {
22+
done := make(chan struct{})
23+
close(done)
24+
stop := make(chan struct{})
25+
close(stop)
26+
return &Client{
27+
addr: streamAddr,
28+
handler: handler,
29+
done: done,
30+
stop: stop,
31+
}
32+
}
33+
34+
func (c *Client) connect() (*net.TCPConn, error) {
35+
const maxConnAttempts = 7
36+
var (
37+
conn net.Conn
38+
err error
39+
)
40+
41+
for i := range maxConnAttempts {
42+
if i > 0 {
43+
time.Sleep((time.Millisecond * 100) << (i - 1))
44+
}
45+
conn, err = net.Dial("tcp", c.addr)
46+
if err != nil {
47+
continue
48+
}
49+
break
50+
}
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
return conn.(*net.TCPConn), nil
56+
}
57+
58+
func (c *Client) loop() {
59+
for {
60+
var ev Event
61+
err := ev.read(c.conn)
62+
if err == nil {
63+
c.handler(ev)
64+
continue
65+
}
66+
select {
67+
case <-c.stop:
68+
return
69+
default:
70+
cliLog.Errorf("client loop %s", err)
71+
return
72+
}
73+
}
74+
}
75+
76+
func (c *Client) Start() error {
77+
select {
78+
case <-c.done:
79+
c.done = make(chan struct{})
80+
default:
81+
return fmt.Errorf("client running")
82+
}
83+
84+
conn, err := c.connect()
85+
if err != nil {
86+
close(c.done)
87+
return err
88+
}
89+
90+
c.conn = conn
91+
c.stop = make(chan struct{})
92+
93+
go func() {
94+
c.loop()
95+
close(c.done)
96+
}()
97+
98+
return nil
99+
}
100+
101+
func (c *Client) Stop() {
102+
select {
103+
case <-c.stop:
104+
return
105+
default:
106+
}
107+
108+
close(c.stop)
109+
c.conn.Close()
110+
<-c.done
111+
112+
c.conn = nil
113+
}

debugstream/debug.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build debug
2+
3+
package debugstream
4+
5+
// Stream is the real implementation of the debug stream, [StreamServer] when
6+
// compiling with debug tag.
7+
type Stream = StreamServer
8+
9+
func New() *Stream {
10+
return NewStreamServer()
11+
}

debugstream/event.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package debugstream
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"io"
7+
)
8+
9+
// The following are guard event codes, that can be used to assert debug events
10+
// in tests.
11+
const (
12+
DEStart = iota + 1
13+
DEShutdown
14+
)
15+
16+
// Event is the message that is sent from the Stream to the Client.
17+
type Event struct {
18+
Code uint64
19+
Data []byte
20+
}
21+
22+
func (e *Event) write(w io.Writer) error {
23+
err := binary.Write(w, binary.BigEndian, e.Code)
24+
if err != nil {
25+
return err
26+
}
27+
dataLen := uint64(len(e.Data))
28+
err = binary.Write(w, binary.BigEndian, dataLen)
29+
if err != nil {
30+
return err
31+
}
32+
n, err := w.Write(e.Data)
33+
if err != nil {
34+
return err
35+
}
36+
if n != int(dataLen) {
37+
return fmt.Errorf("nWrite != dataLen")
38+
}
39+
return nil
40+
}
41+
42+
func (e *Event) read(r io.Reader) error {
43+
err := binary.Read(r, binary.BigEndian, &e.Code)
44+
if err != nil {
45+
return err
46+
}
47+
var dataLen uint64
48+
err = binary.Read(r, binary.BigEndian, &dataLen)
49+
if err != nil {
50+
return err
51+
}
52+
e.Data = make([]byte, dataLen)
53+
n, err := io.ReadFull(r, e.Data)
54+
if err != nil {
55+
return err
56+
}
57+
if n != int(dataLen) {
58+
return fmt.Errorf("nRead != dataLen")
59+
}
60+
return nil
61+
}

debugstream/log.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package debugstream
2+
3+
import "github.com/btcsuite/btclog"
4+
5+
var strLog btclog.Logger
6+
var cliLog btclog.Logger
7+
8+
func init() {
9+
DisableLog()
10+
}
11+
12+
// DisableLog disables all library log output. Logging output is disabled
13+
// by default until either UseLogger or SetLogWriter are called.
14+
func DisableLog() {
15+
strLog, cliLog = btclog.Disabled, btclog.Disabled
16+
}
17+
18+
// UseLogger uses a specified Logger to output package logging info.
19+
// This should be used in preference to SetLogWriter if the caller is also
20+
// using btclog.
21+
func UseLoggers(strLogger, cliLogger btclog.Logger) {
22+
strLog, cliLog = strLogger, cliLogger
23+
}

debugstream/nop.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !debug
2+
3+
package debugstream
4+
5+
// Stream is a [StreamNOP] when debug flag is not set, effectively doing nothing
6+
// because all its procedures are also NOP.
7+
type Stream = StreamNOP
8+
9+
func New() *Stream {
10+
return NewStreamNOP()
11+
}

0 commit comments

Comments
 (0)