Skip to content

Commit ea5959a

Browse files
committed
integration/rpctest: do not ignore exit errors and allow harness restart
Now, Harness TearDown procedure returns an error if the btcd process stopped with error status code. Also, SkipCleanup option was added to TearDown, so now we can write tests that restart the node while keeping the state.
1 parent f6c8d3f commit ea5959a

4 files changed

Lines changed: 137 additions & 28 deletions

File tree

integration/rpctest/node.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package rpctest
66

77
import (
8+
"errors"
89
"fmt"
910
"log"
1011
"os"
@@ -179,12 +180,23 @@ func newNode(config *nodeConfig, dataDir string) (*node, error) {
179180
}, nil
180181
}
181182

183+
func (n *node) started() bool {
184+
if n.pidFile != "" {
185+
return true
186+
}
187+
return false
188+
}
189+
182190
// start creates a new btcd process, and writes its pid in a file reserved for
183191
// recording the pid of the launched process. This file can be used to
184192
// terminate the process in case of a hang, or panic. In the case of a failing
185193
// test case, or panic, it is important that the process be stopped via stop(),
186194
// otherwise, it will persist unless explicitly killed.
187195
func (n *node) start() error {
196+
if n.started() {
197+
return fmt.Errorf("node already started")
198+
}
199+
188200
if err := n.cmd.Start(); err != nil {
189201
return err
190202
}
@@ -208,31 +220,39 @@ func (n *node) start() error {
208220

209221
// stop interrupts the running btcd process process, and waits until it exits
210222
// properly. On windows, interrupt is not supported, so a kill signal is used
211-
// instead
223+
// instead.
212224
func (n *node) stop() error {
225+
var multiErr error
226+
213227
if n.cmd == nil || n.cmd.Process == nil {
214-
// return if not properly initialized
215-
// or error starting the process
228+
// return if not properly initialized or error starting the
229+
// process.
216230
return nil
217231
}
218-
defer n.cmd.Wait()
232+
219233
if runtime.GOOS == "windows" {
220-
return n.cmd.Process.Signal(os.Kill)
234+
multiErr = errors.Join(n.cmd.Process.Signal(os.Kill), multiErr)
235+
} else {
236+
multiErr = errors.Join(n.cmd.Process.Signal(os.Interrupt),
237+
multiErr)
221238
}
222-
return n.cmd.Process.Signal(os.Interrupt)
239+
240+
multiErr = errors.Join(n.cmd.Wait(), multiErr)
241+
242+
return multiErr
223243
}
224244

225245
// cleanup cleanups process and args files. The file housing the pid of the
226246
// created process will be deleted, as well as any directories created by the
227247
// process.
228248
func (n *node) cleanup() error {
229-
if n.pidFile != "" {
230-
if err := os.Remove(n.pidFile); err != nil {
231-
log.Printf("unable to remove file %s: %v", n.pidFile,
232-
err)
233-
}
249+
if err := os.Remove(n.pidFile); err != nil {
250+
log.Printf("unable to remove file %s: %v", n.pidFile, err)
234251
}
235252

253+
n.pidFile = ""
254+
n.cmd = n.config.command()
255+
236256
// Since the node's main data directory is passed in to the node config,
237257
// it isn't our responsibility to clean it up. So we're done after
238258
// removing the pid file.
@@ -242,13 +262,24 @@ func (n *node) cleanup() error {
242262
// shutdown terminates the running btcd process, and cleans up all
243263
// file/directories created by node.
244264
func (n *node) shutdown() error {
245-
if err := n.stop(); err != nil {
246-
return err
265+
var multiErr error
266+
267+
if !n.started() {
268+
return nil
247269
}
270+
271+
exitErr := &exec.ExitError{}
272+
if err := n.stop(); err != nil && errors.As(err, &exitErr) {
273+
multiErr = errors.Join(err, multiErr)
274+
} else if err != nil {
275+
return errors.Join(err, multiErr)
276+
}
277+
248278
if err := n.cleanup(); err != nil {
249-
return err
279+
return errors.Join(err, multiErr)
250280
}
251-
return nil
281+
282+
return multiErr
252283
}
253284

254285
// genCertPair generates a key/cert pair to the paths provided.

integration/rpctest/rpc_harness.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
package rpctest
66

77
import (
8+
"errors"
89
"fmt"
910
"math/rand/v2"
1011
"net"
1112
"os"
13+
"os/exec"
1214
"path/filepath"
1315
"strconv"
1416
"sync"
@@ -276,7 +278,7 @@ func New(opts ...HarnessOpts) (*Harness, error) {
276278
// goroutine as they are not concurrent safe.
277279
func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
278280
// Start the btcd node itself. This spawns a new process which will be
279-
// managed
281+
// managed.
280282
if err := h.node.start(); err != nil {
281283
return fmt.Errorf("error starting node: %w", err)
282284
}
@@ -328,11 +330,13 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
328330
return nil
329331
}
330332

331-
// tearDown stops the running rpc test instance. All created processes are
332-
// killed, and temporary directories removed.
333+
// tearDown stops the running rpc test instance. All created processes are
334+
// killed, and temporary directories removed if skipCleanup is false.
333335
//
334336
// This function MUST be called with the harness state mutex held (for writes).
335-
func (h *Harness) tearDown() error {
337+
func (h *Harness) tearDown(skipCleanup bool) error {
338+
var multiErr error
339+
336340
if h.Client != nil {
337341
h.Client.Shutdown()
338342
h.Client.WaitForShutdown()
@@ -343,29 +347,45 @@ func (h *Harness) tearDown() error {
343347
h.BatchClient.WaitForShutdown()
344348
}
345349

346-
if err := h.node.shutdown(); err != nil {
347-
return err
350+
// In the case of exit errors we still perform the cleanup, but also
351+
// return the exit error to the caller.
352+
exitErr := &exec.ExitError{}
353+
if err := h.node.shutdown(); err != nil && errors.As(err, &exitErr) {
354+
multiErr = errors.Join(err, multiErr)
355+
} else if err != nil {
356+
return errors.Join(err, multiErr)
348357
}
349358

350-
if err := os.RemoveAll(h.testNodeDir); err != nil {
351-
return err
359+
if !skipCleanup {
360+
if err := os.RemoveAll(h.testNodeDir); err != nil {
361+
return errors.Join(err, multiErr)
362+
}
363+
364+
delete(testInstances, h.testNodeDir)
352365
}
353366

354-
delete(testInstances, h.testNodeDir)
367+
return multiErr
368+
}
355369

356-
return nil
370+
type HTearDownOpts struct {
371+
SkipCleanup bool
357372
}
358373

359374
// TearDown stops the running rpc test instance. All created processes are
360375
// killed, and temporary directories removed.
361376
//
362377
// NOTE: This method and SetUp should always be called from the same goroutine
363378
// as they are not concurrent safe.
364-
func (h *Harness) TearDown() error {
379+
func (h *Harness) TearDown(opts ...HTearDownOpts) error {
365380
harnessStateMtx.Lock()
366381
defer harnessStateMtx.Unlock()
367382

368-
return h.tearDown()
383+
var o HTearDownOpts
384+
if len(opts) > 0 {
385+
o = opts[0]
386+
}
387+
388+
return h.tearDown(o.SkipCleanup)
369389
}
370390

371391
// connectRPCClient attempts to establish an RPC connection to the created btcd

integration/rpctest/rpc_harness_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,62 @@ func testMemWalletLockedOutputs(r *Harness, t *testing.T) {
546546
}
547547
}
548548

549+
func testSkipCleanup(_ *Harness, t *testing.T) {
550+
h, err := New()
551+
if err != nil {
552+
t.Fatal(err)
553+
}
554+
if err := h.SetUp(true, 1); err != nil {
555+
t.Fatal(err)
556+
}
557+
defer h.TearDown()
558+
count, err := h.Client.GetBlockCount()
559+
if err != nil {
560+
t.Fatal(err)
561+
}
562+
if count != 101 {
563+
t.Fatalf("unexpected blockcount %d", count)
564+
}
565+
566+
// Verify that the state is kept between restarts.
567+
for range 3 {
568+
if err := h.TearDown(HTearDownOpts{SkipCleanup: true}); err != nil {
569+
570+
t.Fatal(err)
571+
}
572+
if err := h.SetUp(false, 0); err != nil {
573+
t.Fatal(err)
574+
}
575+
}
576+
count, err = h.Client.GetBlockCount()
577+
if err != nil {
578+
t.Fatal(err)
579+
}
580+
if count != 101 {
581+
t.Fatalf("unexpected blockcount %d", count)
582+
}
583+
}
584+
585+
func testTearDownReturnsErrorWhenExitCodeIsNotZero(_ *Harness, t *testing.T) {
586+
// The invalid flag should make the startup fail.
587+
h, err := New(HarnessOpts{ExtraArgs: []string{"--invalidflag=0"}})
588+
if err != nil {
589+
t.Fatal(err)
590+
}
591+
592+
// Make sure we fail quick.
593+
h.ConnectionRetryTimeout = time.Millisecond * 100
594+
h.MaxConnRetries = 1
595+
if err := h.SetUp(false, 0); err == nil {
596+
t.Fatal(err)
597+
}
598+
599+
// We expect the process to exit with non zero status code.
600+
if err := h.TearDown(); err == nil {
601+
t.Fatal(err)
602+
}
603+
}
604+
549605
var harnessTestCases = []HarnessTestCase{
550606
testSendOutputs,
551607
testConnectNode,
@@ -556,6 +612,8 @@ var harnessTestCases = []HarnessTestCase{
556612
testGenerateAndSubmitBlockWithCustomCoinbaseOutputs,
557613
testMemWalletReorg,
558614
testMemWalletLockedOutputs,
615+
testSkipCleanup,
616+
testTearDownReturnsErrorWhenExitCodeIsNotZero,
559617
}
560618

561619
var mainHarness *Harness

integration/rpctest/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func TearDownAll() error {
140140
defer harnessStateMtx.Unlock()
141141

142142
for _, harness := range testInstances {
143-
if err := harness.tearDown(); err != nil {
143+
if err := harness.tearDown(false); err != nil {
144144
return err
145145
}
146146
}

0 commit comments

Comments
 (0)