Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

loadtest: add multi send test #558

Merged
merged 3 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 22 additions & 24 deletions itest/loadtest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/jessevdk/go-flags"
"github.com/lightninglabs/taproot-assets/taprpc"
)

const (
Expand Down Expand Up @@ -58,9 +59,21 @@ type Config struct {
// Bitcoin is the configuration for the bitcoin backend.
Bitcoin *BitcoinConfig `group:"bitcoin" namespace:"bitcoin" long:"bitcoin" description:"bitcoin client configuration"`

// BatchSize is the number of assets to mint in a single batch. This is only
// relevant for some test cases.
BatchSize int `long:"batch-size" description:"the number of assets to mint in a single batch"`
// BatchSize is the number of assets to mint in a single batch. This is
// only relevant for the mint test.
BatchSize int `long:"mint-test-batch-size" description:"the number of assets to mint in a single batch; only relevant for the mint test"`

// NumSends is the number of asset sends to perform. This is only
// relevant for the send test.
NumSends int `long:"send-test-num-sends" description:"the number of send operations to perform; only relevant for the send test"`

// NumAssets is the number of assets to send in each send operation.
// This is only relevant for the send test.
NumAssets uint64 `long:"send-test-num-assets" description:"the number of assets to send in each send operation; only relevant for the send test"`

// SendType is the type of asset to attempt to send. This is only
// relevant for the send test.
SendType taprpc.AssetType `long:"send-test-send-type" description:"the type of asset to attempt to send; only relevant for the send test"`

// TestSuiteTimeout is the timeout for the entire test suite.
TestSuiteTimeout time.Duration `long:"test-suite-timeout" description:"the timeout for the entire test suite"`
Expand All @@ -73,7 +86,6 @@ type Config struct {
// binary.
func DefaultConfig() Config {
return Config{
TestCases: []string{"mint_batch_stress"},
Alice: &User{
Tapd: &TapConfig{
Name: "alice",
Expand All @@ -85,6 +97,9 @@ func DefaultConfig() Config {
},
},
BatchSize: 100,
NumSends: 50,
NumAssets: 1, // We only mint collectibles.
SendType: taprpc.AssetType_COLLECTIBLE,
TestSuiteTimeout: defaultSuiteTimeout,
TestTimeout: defaultTestTimeout,
}
Expand All @@ -95,21 +110,11 @@ func DefaultConfig() Config {
//
// The configuration proceeds as follows:
// 1. Start with a default config with sane settings
// 2. Pre-parse the command line to check for an alternative config file
// 3. Load configuration file overwriting defaults with any specified options
// 4. Parse CLI options and overwrite/add any specified options
// 2. Load configuration file overwriting defaults with any specified options
func LoadConfig() (*Config, error) {
// Pre-parse the command line options to pick up an alternative config
// file.
preCfg := DefaultConfig()
if _, err := flags.Parse(&preCfg); err != nil {
return nil, err
}

// Next, load any additional configuration options from the file.
cfg := preCfg
// First, load any additional configuration options from the file.
cfg := DefaultConfig()
fileParser := flags.NewParser(&cfg, flags.Default)

err := flags.NewIniParser(fileParser).ParseFile(defaultConfigPath)
if err != nil {
// If it's a parsing related error, then we'll return
Expand All @@ -120,13 +125,6 @@ func LoadConfig() (*Config, error) {
}
}

// Finally, parse the remaining command line options again to ensure
// they take precedence.
flagParser := flags.NewParser(&cfg, flags.Default)
if _, err := flagParser.Parse(); err != nil {
return nil, err
}

// Make sure everything we just loaded makes sense.
cleanCfg, err := ValidateConfig(cfg)
if err != nil {
Expand Down
65 changes: 51 additions & 14 deletions itest/loadtest/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ import (
"github.com/stretchr/testify/require"
)

type testCase struct {
name string
fn func(t *testing.T, ctx context.Context, cfg *Config)
}

var loadTestCases = []testCase{
{
name: "mint",
fn: mintTest,
},
{
name: "send",
fn: sendTest,
},
}

// TestPerformance executes the configured performance tests.
func TestPerformance(t *testing.T) {
cfg, err := LoadConfig()
Expand All @@ -18,23 +34,44 @@ func TestPerformance(t *testing.T) {
ctxt, cancel := context.WithTimeout(ctxb, cfg.TestSuiteTimeout)
defer cancel()

for _, testCase := range cfg.TestCases {
execTestCase(t, ctxt, testCase, cfg)
}
}
for _, tc := range loadTestCases {
tc := tc

// execTestCase is the method in charge of executing a single test case.
func execTestCase(t *testing.T, ctx context.Context, testName string,
cfg *Config) {
if !shouldRunCase(tc.name, cfg.TestCases) {
t.Logf("Not running test case '%s' as not configured",
tc.name)

ctxt, cancel := context.WithTimeout(ctx, cfg.TestTimeout)
defer cancel()
continue
}

switch testName {
case "mint_batch_stress":
execMintBatchStressTest(t, ctxt, cfg)
success := t.Run(tc.name, func(tt *testing.T) {
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
ctxt, cancel := context.WithTimeout(
ctxt, cfg.TestTimeout,
)
defer cancel()

default:
require.Fail(t, "unknown test case: %v", testName)
tc.fn(t, ctxt, cfg)
})
if !success {
t.Fatalf("test case %v failed", tc.name)
}
}
}

// shouldRunCase returns true if the given test case should be run. This will
// return true if the config file does not specify any test cases. In that case
// we can select the test cases to run using the command line
// (-test.run="TestPerformance/test_case_name")
func shouldRunCase(name string, configuredCases []string) bool {
if len(configuredCases) == 0 {
return true
}

for _, c := range configuredCases {
if c == name {
return true
}
}

return false
}
54 changes: 10 additions & 44 deletions itest/loadtest/mint_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"testing"
"time"

"github.com/btcsuite/btcd/rpcclient"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/itest"
"github.com/lightninglabs/taproot-assets/taprpc"
Expand All @@ -24,60 +23,27 @@ import (
//go:embed testdata/8k-metadata.hex
var imageMetadataHex []byte

// execMintBatchStressTest checks that we are able to mint a batch of assets
// and that other memebers in the federation see the universe updated
// accordingly.
func execMintBatchStressTest(t *testing.T, ctx context.Context, cfg *Config) {
// Create tapd clients.
alice, aliceCleanUp := getTapClient(t, ctx, cfg.Alice.Tapd)
defer aliceCleanUp()

_, err := alice.GetInfo(ctx, &taprpc.GetInfoRequest{})
require.NoError(t, err)

bob, bobCleanUp := getTapClient(t, ctx, cfg.Bob.Tapd)
defer bobCleanUp()

_, err = bob.GetInfo(ctx, &taprpc.GetInfoRequest{})
require.NoError(t, err)

// Create bitcoin client.
bitcoinClient := getBitcoinConn(t, cfg.Bitcoin)

itest.MineBlocks(t, bitcoinClient, 1, 0)

// If we fail from this point onward, we might have created a
// transaction that isn't mined yet. To make sure we can run the test
// again, we'll make sure to clean up the mempool by mining a block.
t.Cleanup(func() {
itest.MineBlocks(t, bitcoinClient, 1, 0)
})
// mintTest checks that we are able to mint a batch of assets and that other
// members in the federation see the universe updated accordingly.
func mintTest(t *testing.T, ctx context.Context, cfg *Config) {
// Start by initializing all our client connections.
alice, bob, bitcoinClient := initClients(t, ctx, cfg)

imageMetadataBytes, err := hex.DecodeString(
strings.Trim(string(imageMetadataHex), "\n"),
)
require.NoError(t, err)

aliceHost := fmt.Sprintf("%s:%d", cfg.Alice.Tapd.Host,
cfg.Alice.Tapd.Port)

minterTimeout := 10 * time.Minute
mintBatchStressTest(
t, ctx, bitcoinClient, alice, bob, aliceHost, cfg.BatchSize,
imageMetadataBytes, minterTimeout,
)
}

func mintBatchStressTest(t *testing.T, ctx context.Context,
bitcoinClient *rpcclient.Client, alice, bob itest.TapdClient,
aliceHost string, batchSize int, imageMetadataBytes []byte,
minterTimeout time.Duration) {

var (
minterTimeout = cfg.TestTimeout
batchSize = cfg.BatchSize
batchReqs = make([]*mintrpc.MintAssetRequest, batchSize)
baseName = fmt.Sprintf("jpeg-%d", rand.Int31())
metaPrefixSize = binary.MaxVarintLen16
metadataPrefix = make([]byte, metaPrefixSize)
aliceHost = fmt.Sprintf(
"%s:%d", alice.cfg.Host, alice.cfg.Port,
)
)

// Before we mint a new group, let's first find out how many there
Expand Down
129 changes: 129 additions & 0 deletions itest/loadtest/send_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package loadtest

import (
"context"
prand "math/rand"
"testing"

"github.com/btcsuite/btcd/rpcclient"
"github.com/lightninglabs/taproot-assets/itest"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/stretchr/testify/require"
)

var (
statusDetected = taprpc.AddrEventStatus_ADDR_EVENT_STATUS_TRANSACTION_DETECTED
statusCompleted = taprpc.AddrEventStatus_ADDR_EVENT_STATUS_COMPLETED
)

// sendTest checks that we are able to send assets between the two nodes.
func sendTest(t *testing.T, ctx context.Context, cfg *Config) {
// Start by initializing all our client connections.
alice, bob, bitcoinClient := initClients(t, ctx, cfg)

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, cfg.TestTimeout)
defer cancel()

t.Logf("Running send test, sending %d asset(s) of type %v %d times",
cfg.NumAssets, cfg.SendType, cfg.NumSends)
for i := 1; i <= cfg.NumSends; i++ {
send, receive, ok := pickSendNode(
t, ctx, cfg.NumAssets, cfg.SendType, alice, bob,
)
if !ok {
t.Fatalf("Aborting send test at attempt %d of %d as "+
"no node has enough balance to send %d "+
"assets of type %v", i, cfg.NumSends,
cfg.NumAssets, cfg.SendType)
return
}

sendAssets(
t, ctxt, cfg.NumAssets, cfg.SendType, send, receive,
bitcoinClient,
)

t.Logf("Finished %d of %d send operations", i, cfg.NumSends)
}
}

// sendAsset sends the given number of assets of the given type from the given
// node to the other node.
func sendAssets(t *testing.T, ctx context.Context, numAssets uint64,
assetType taprpc.AssetType, send, receive *rpcClient,
bitcoinClient *rpcclient.Client) {

// Query the asset we'll be sending, so we can assert some things about
// it later.
sendAsset := send.assetIDWithBalance(t, ctx, numAssets, assetType)
t.Logf("Sending %d asset(s) with ID %x from %v to %v", numAssets,
sendAsset.AssetGenesis.AssetId, send.cfg.Name, receive.cfg.Name)

// Let's create an address on the receiving node and make sure it's
// created correctly.
addr, err := receive.NewAddr(ctx, &taprpc.NewAddrRequest{
AssetId: sendAsset.AssetGenesis.AssetId,
Amt: numAssets,
})
require.NoError(t, err)
itest.AssertAddrCreated(t, receive, sendAsset, addr)

// Before we send the asset, we record the existing transfers on the
// sending node, so we can easily select the new transfer once it
// appears.
transfersBefore := send.listTransfersSince(t, ctx, nil)

// Initiate the send now.
_, err = send.SendAsset(ctx, &taprpc.SendAssetRequest{
TapAddrs: []string{addr.Encoded},
})
require.NoError(t, err)

// Wait for the transfer to appear on the sending node.
require.Eventually(t, func() bool {
newTransfers := send.listTransfersSince(t, ctx, transfersBefore)
return len(newTransfers) == 1
}, defaultTimeout, wait.PollInterval)

// And for it to be detected on the receiving node.
itest.AssertAddrEvent(t, receive, addr, 1, statusDetected)

// Mine a block to confirm the transfer.
itest.MineBlocks(t, bitcoinClient, 1, 1)

// Now the transfer should go to completed eventually.
itest.AssertAddrEvent(t, receive, addr, 1, statusCompleted)
}

// pickSendNode picks a node at random, checks whether it has enough assets of
// the given type, and returns it. The second return value is the other node,
// which will be the receiving node. The boolean argument returns true if there
// is a node with sufficient balance. If that is false, the test should be
// skipped.
func pickSendNode(t *testing.T, ctx context.Context, minBalance uint64,
assetType taprpc.AssetType, a, b *rpcClient) (*rpcClient, *rpcClient,
bool) {

send, receive := a, b
if prand.Intn(1) == 0 {
send, receive = b, a
}

// Check if the randomly picked send node has enough balance.
if send.assetIDWithBalance(t, ctx, minBalance, assetType) != nil {
return send, receive, true
}

// If we get here, the send node doesn't have enough balance. We'll try
// the other one.
send, receive = receive, send
if send.assetIDWithBalance(t, ctx, minBalance, assetType) != nil {
return send, receive, true
}

// None of the nodes have enough balance. We can't run the send test
// currently.
return nil, nil, false
}
Loading