Skip to content

Commit

Permalink
Merge pull request #558 from lightninglabs/loadtest-send-asset
Browse files Browse the repository at this point in the history
loadtest: add multi send test
  • Loading branch information
Roasbeef authored Oct 10, 2023
2 parents d63d1b3 + ba9fa8c commit abdfae2
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 88 deletions.
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) {
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

0 comments on commit abdfae2

Please sign in to comment.