itest: add itest framework and basic tests

This commit is contained in:
Oliver Gugger 2025-07-01 21:37:53 +02:00
parent f794333c0b
commit d7e03409d5
No known key found for this signature in database
GPG key ID: 8E4256593F177720
4 changed files with 233 additions and 7 deletions

149
itest/chantools_harness.go Normal file
View file

@ -0,0 +1,149 @@
package itest
import (
"bufio"
"bytes"
"errors"
"io"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// ChantoolsProcess wraps a running chantools process for integration testing.
type ChantoolsProcess struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout *os.File
stdoutReader *bufio.Reader
stderr *bufio.Reader
}
// StartChantools starts the chantools binary with the given arguments.
func StartChantools(t *testing.T, args ...string) *ChantoolsProcess {
t.Helper()
args = append([]string{"--nologfile"}, args...)
cmd := exec.Command("chantools", args...)
stdin, err := cmd.StdinPipe()
require.NoError(t, err)
stdoutPipe, err := cmd.StdoutPipe()
require.NoError(t, err)
stderrPipe, err := cmd.StderrPipe()
require.NoError(t, err)
stdoutFile, ok := stdoutPipe.(*os.File)
require.True(t, ok)
require.NoError(t, cmd.Start())
return &ChantoolsProcess{
cmd: cmd,
stdin: stdin,
stdout: stdoutFile,
stdoutReader: bufio.NewReader(stdoutFile),
stderr: bufio.NewReader(stderrPipe),
}
}
// WriteInput writes input to the process's stdin.
func (p *ChantoolsProcess) WriteInput(t *testing.T, input string) {
t.Helper()
_, err := io.WriteString(p.stdin, input)
require.NoError(t, err, "failed to write input to chantools")
}
// ReadAllOutput reads all output from the process's stdout until EOF.
func (p *ChantoolsProcess) ReadAllOutput(t *testing.T) string {
t.Helper()
resp, err := io.ReadAll(p.stdout)
require.NoError(t, err, "failed to read chantools output")
log.Debugf("[CHANTOOLS]: %s", resp)
return string(resp)
}
// ReadOutputUntil reads from stdout until the given substring is found or
// timeout.
func (p *ChantoolsProcess) ReadOutputUntil(t *testing.T, substr string,
timeout time.Duration) string {
t.Helper()
var out bytes.Buffer
deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
t.Fatal("timeout waiting for chantools output")
}
line, err := p.stdoutReader.ReadString('\n')
out.WriteString(line)
log.Debugf("[CHANTOOLS]: %s", line)
if strings.Contains(out.String(), substr) {
return out.String()
}
require.NoError(t, err)
}
}
// ReadAvailableOutput reads as many bytes as possible from stdout until the
// timeout elapses.
func (p *ChantoolsProcess) ReadAvailableOutput(t *testing.T,
timeout time.Duration) string {
t.Helper()
err := p.stdout.SetReadDeadline(time.Now().Add(timeout))
require.NoError(t, err, "failed to set read deadline")
defer func() {
_ = p.stdout.SetReadDeadline(time.Time{})
}()
var out bytes.Buffer
for {
buf := make([]byte, 1024)
n, err := p.stdoutReader.Read(buf)
if n > 0 {
chunk := string(buf[:n])
out.WriteString(chunk)
}
if err != nil {
if errors.Is(err, io.EOF) ||
errors.Is(err, os.ErrDeadlineExceeded) {
break
}
time.Sleep(50 * time.Millisecond)
}
}
log.Debugf("[CHANTOOLS]: %s", out.String())
return out.String()
}
// Wait waits for the process to exit.
func (p *ChantoolsProcess) Wait(t *testing.T) {
t.Helper()
require.NoError(t, p.cmd.Wait())
}
// Kill kills the process.
func (p *ChantoolsProcess) Kill(t *testing.T) {
t.Helper()
require.NoError(t, p.cmd.Process.Kill())
}

16
itest/log.go Normal file
View file

@ -0,0 +1,16 @@
package itest
import (
"os"
"github.com/btcsuite/btclog/v2"
)
var log btclog.Logger
//nolint:gochecknoinits
func init() {
logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
logger.SetLevel(btclog.LevelTrace)
log = logger.SubSystem("ITEST")
}

57
itest/standalone_test.go Normal file
View file

@ -0,0 +1,57 @@
package itest
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
const (
testXPriv = "xprv9s21ZrQH143K2ZzuN99NjD7oJruBomAVbNDXzPmhHYcwf8WXCsML" +
"63azyS1rzzfpUsLifeDkM4Q6U9PF9RP7frSGKkMfDTDiiyQjH2PUj2z"
testMnemonic = "about wolf boost other battle asthma refuse wedding " +
"few purchase track one smooth tunnel immune glass infant " +
"tag manual multiply diagram orient wrist agent"
)
var (
readTimeout = 100 * time.Millisecond
defaultTimeout = 5 * time.Second
)
func TestChantoolsShowRootKeyXPriv(t *testing.T) {
proc := StartChantools(t, "showrootkey", "--rootkey", testXPriv)
defer proc.Kill(t)
output := proc.ReadOutputUntil(
t, "Your BIP32 HD root key is:", defaultTimeout,
)
require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv)
}
func TestChantoolsShowRootKeyMnemonic(t *testing.T) {
proc := StartChantools(t, "showrootkey")
defer proc.Kill(t)
go func() {
errString, err := proc.stderr.ReadString('\n')
log.Errorf("chantools stderr: %v, error: %v", errString, err)
}()
mnemonicPrompt := proc.ReadAvailableOutput(t, readTimeout)
require.Contains(t, mnemonicPrompt, "Input your 24-word mnemonic")
proc.WriteInput(t, testMnemonic+"\n")
passphrasePrompt := proc.ReadAvailableOutput(t, readTimeout)
require.Contains(
t, passphrasePrompt, "Input your cipher seed passphrase",
)
proc.WriteInput(t, "\n")
output := proc.ReadOutputUntil(
t, "Your BIP32 HD root key is:", defaultTimeout,
)
require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv)
}

View file

@ -2,6 +2,7 @@ package lnd
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
@ -139,16 +140,15 @@ func ReadPassphrase(verb string) ([]byte, error) {
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case passphrase == "":
fmt.Printf("Input your cipher seed passphrase (press enter "+
"if your seed %s a passphrase): ", verb)
var err error
passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), //nolint
passphraseBytes, err = PasswordFromConsole(
fmt.Sprintf("Input your cipher seed passphrase "+
"(press enter if your seed %s a passphrase): ",
verb),
)
if err != nil {
return nil, err
}
fmt.Println()
// There was a password in the environment, just convert it to bytes.
default:
@ -160,13 +160,15 @@ func ReadPassphrase(verb string) ([]byte, error) {
// PasswordFromConsole reads a password from the console or stdin.
func PasswordFromConsole(userQuery string) ([]byte, error) {
fmt.Print(userQuery)
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
@ -177,7 +179,9 @@ func PasswordFromConsole(userQuery string) ([]byte, error) {
if err != nil {
return nil, err
}
return pw, nil
fmt.Println()
return bytes.TrimSpace(pw), nil
}
// OpenWallet opens a lnd compatible wallet and returns it, along with the