Skip to content
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
35 changes: 35 additions & 0 deletions daemon/test_daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/bsv-blockchain/teranode/stores/blob/options"
"github.com/bsv-blockchain/teranode/stores/utxo"
"github.com/bsv-blockchain/teranode/stores/utxo/fields"
"github.com/bsv-blockchain/teranode/test/utils/containers"
"github.com/bsv-blockchain/teranode/test/utils/transactions"
"github.com/bsv-blockchain/teranode/test/utils/wait"
"github.com/bsv-blockchain/teranode/ulogger"
Expand Down Expand Up @@ -77,6 +78,7 @@ type TestDaemon struct {
UtxoStore utxo.Store
P2PClient p2p.ClientI
composeDependencies tc.ComposeStack
containerManager *containers.ContainerManager
ctxCancel context.CancelFunc
d *Daemon
privKey *bec.PrivateKey
Expand All @@ -95,6 +97,9 @@ type TestOptions struct {
SkipRemoveDataDir bool
StartDaemonDependencies bool
FSMState blockchain.FSMStateType
// UTXOStoreType specifies which UTXO store backend to use ("aerospike", "postgres")
// If empty, defaults to "aerospike"
UTXOStoreType string
}

// JSONError represents a JSON error response from the RPC server.
Expand Down Expand Up @@ -314,6 +319,28 @@ func NewTestDaemon(t *testing.T, opts TestOptions) *TestDaemon {
opts.SettingsOverrideFunc(appSettings)
}

// Initialize container manager for UTXO store if UTXOStoreType is specified
var containerManager *containers.ContainerManager
if opts.UTXOStoreType != "" {
containerManager, err = containers.NewContainerManager(containers.UTXOStoreType(opts.UTXOStoreType))
require.NoError(t, err, "Failed to create container manager")

utxoStoreURL, err := containerManager.Initialize(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource leak: If daemon initialization fails after this point, the container won't be cleaned up because td.Stop() is never called. Register cleanup with t.Cleanup() immediately after container initialization to prevent leaks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed: Cleanup is now registered with t.Cleanup() immediately after container initialization (line 332-336).

require.NoError(t, err, "Failed to initialize container")

// Register cleanup immediately to prevent resource leak if daemon initialization fails
t.Cleanup(func() {
if containerManager != nil {
_ = containerManager.Cleanup()
}
})

// Override the UTXO store URL in settings
appSettings.UtxoStore.UtxoStore = utxoStoreURL

t.Logf("Initialized %s container with URL: %s", opts.UTXOStoreType, utxoStoreURL.String())
}

readyCh := make(chan struct{})

var (
Expand Down Expand Up @@ -481,6 +508,7 @@ func NewTestDaemon(t *testing.T, opts TestOptions) *TestDaemon {
UtxoStore: utxoStore,
P2PClient: p2pClient,
composeDependencies: composeDependencies,
containerManager: containerManager,
ctxCancel: cancel,
d: d,
privKey: pk,
Expand All @@ -506,6 +534,13 @@ func (td *TestDaemon) Stop(t *testing.T, skipTracerShutdown ...bool) {
// Cleanup daemon stores to reset singletons
td.d.daemonStores.Cleanup()

// Cleanup container manager if it exists
if td.containerManager != nil {
if err := td.containerManager.Cleanup(); err != nil {
t.Logf("Warning: Failed to cleanup container manager: %v", err)
}
}

WaitForPortsFree(t, td.Ctx, td.Settings)

// cleanup remaining listeners that were never used
Expand Down
171 changes: 86 additions & 85 deletions test/.claude-context/teranode-test-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,151 +72,152 @@ func TestMyScenario(t *testing.T) {
### IMPORTANT: Database Backend Requirements

All tests MUST be tested with multiple database backends to ensure compatibility:
- **SQLite** (in-memory, for fast tests)

- **PostgreSQL** (using testcontainers)
- **Aerospike** (using testcontainers)

### Database Backend Test Pattern
### Unified Container Management (RECOMMENDED)

**NEW: As of 2025, use the unified `UTXOStoreType` option in TestDaemon for automatic container management.**

This approach eliminates boilerplate container initialization code and ensures consistent setup across all tests.

#### Simple Pattern - Single Backend Test

```go
package smoke

import (
"os"
"testing"

"github.com/bsv-blockchain/teranode/daemon"
"github.com/bsv-blockchain/teranode/test/utils/aerospike"
"github.com/bsv-blockchain/teranode/test/utils/postgres"
"github.com/stretchr/testify/require"
)

func init() {
os.Setenv("SETTINGS_CONTEXT", "test")
}

// Test with SQLite (in-memory)
func TestMyFeatureSQLite(t *testing.T) {
utxoStore := "sqlite:///test"
func TestMyFeature(t *testing.T) {
SharedTestLock.Lock()
defer SharedTestLock.Unlock()

t.Run("scenario1", func(t *testing.T) {
testScenario1(t, utxoStore)
})
t.Run("scenario2", func(t *testing.T) {
testScenario2(t, utxoStore)
// Container automatically initialized and cleaned up
td := daemon.NewTestDaemon(t, daemon.TestOptions{
EnableRPC: true,
EnableValidator: true,
SettingsContext: "dev.system.test",
UTXOStoreType: "aerospike", // "aerospike", "postgres"
})
}
defer td.Stop(t)

// Test with PostgreSQL
func TestMyFeaturePostgres(t *testing.T) {
// Setup PostgreSQL container
utxoStore, teardown, err := postgres.SetupTestPostgresContainer()
err := td.BlockchainClient.Run(td.Ctx, "test")
require.NoError(t, err)

defer func() {
_ = teardown()
}()
// Test implementation...
}
```

#### Multi-Backend Pattern - Testing All Backends

```go
package smoke

import (
"testing"
"github.com/bsv-blockchain/teranode/daemon"
"github.com/stretchr/testify/require"
)

// Test with Aerospike (default, recommended for most tests)
func TestMyFeatureAerospike(t *testing.T) {
t.Run("scenario1", func(t *testing.T) {
testScenario1(t, utxoStore)
testScenario1(t, "aerospike")
})
t.Run("scenario2", func(t *testing.T) {
testScenario2(t, utxoStore)
testScenario2(t, "aerospike")
})
}

// Test with Aerospike
func TestMyFeatureAerospike(t *testing.T) {
// Setup Aerospike container
utxoStore, teardown, err := aerospike.InitAerospikeContainer()
require.NoError(t, err)

t.Cleanup(func() {
_ = teardown()
})

// Test with PostgreSQL
func TestMyFeaturePostgres(t *testing.T) {
t.Run("scenario1", func(t *testing.T) {
testScenario1(t, utxoStore)
testScenario1(t, "postgres")
})
t.Run("scenario2", func(t *testing.T) {
testScenario2(t, utxoStore)
testScenario2(t, "postgres")
})
}

// Shared test implementation
func testScenario1(t *testing.T, utxoStore string) {
func testScenario1(t *testing.T, storeType string) {
SharedTestLock.Lock()
defer SharedTestLock.Unlock()

td := daemon.NewTestDaemon(t, daemon.TestOptions{
EnableRPC: true,
EnableValidator: true,
SettingsContext: "dev.system.test",
SettingsOverrideFunc: func(s *settings.Settings) {
url, err := url.Parse(utxoStore)
require.NoError(t, err)
s.UtxoStore.UtxoStore = url
},
UTXOStoreType: storeType, // Automatic container management
})
defer td.Stop(t)

err := td.BlockchainClient.Run(td.Ctx, "test")
require.NoError(t, err)

// Test implementation...
}
```

### Database URL Formats
#### Available Store Types

- **SQLite**: `"sqlite:///test"` (in-memory database)
- **PostgreSQL**: Connection string returned by `SetupTestPostgresContainer()`
- **Aerospike**: URL returned by `InitAerospikeContainer()` (e.g., `"aerospike://host:port/namespace?set=test&expiration=10m"`)
- `"aerospike"` - Aerospike container (production-like, recommended)
- `"postgres"` - PostgreSQL container (production-like)
- `""` (empty) - No automatic container (uses default settings)

### Helper Functions for Database Setup
#### Benefits of Unified Approach

#### PostgreSQL Container Setup
✅ **No boilerplate** - No manual container initialization
✅ **Automatic cleanup** - Containers cleaned up with `td.Stop(t)`
✅ **Consistent setup** - Same initialization across all tests
✅ **Type-safe** - Compile-time validation of store types
✅ **Less code** - ~10 lines reduced per test

```go
import "github.com/bsv-blockchain/teranode/test/utils/postgres"
### Legacy Pattern (Manual Container Setup)

utxoStore, teardown, err := postgres.SetupTestPostgresContainer()
require.NoError(t, err)
defer func() {
_ = teardown()
}()
```
**NOTE: This pattern is deprecated. Use `UTXOStoreType` option instead.**

If you encounter existing tests using manual container setup, they should be refactored to use the unified approach above.

#### Aerospike Container Setup
<details>
<summary>Click to see legacy pattern (for reference only)</summary>

```go
import "github.com/bsv-blockchain/teranode/test/utils/aerospike"
// OLD PATTERN - Do not use for new tests
func TestMyFeatureAerospike(t *testing.T) {
// Manual container setup (deprecated)
utxoStore, teardown, err := aerospike.InitAerospikeContainer()
require.NoError(t, err)
t.Cleanup(func() {
_ = teardown()
})

utxoStore, teardown, err := aerospike.InitAerospikeContainer()
require.NoError(t, err)
t.Cleanup(func() {
_ = teardown()
})
td := daemon.NewTestDaemon(t, daemon.TestOptions{
SettingsContext: "dev.system.test",
SettingsOverrideFunc: func(s *settings.Settings) {
url, err := url.Parse(utxoStore)
require.NoError(t, err)
s.UtxoStore.UtxoStore = url
},
})
defer td.Stop(t)
// ...
}
```

### Configuring TestDaemon with Custom UTXO Store

```go
td := daemon.NewTestDaemon(t, daemon.TestOptions{
SettingsContext: "dev.system.test",
SettingsOverrideFunc: func(s *settings.Settings) {
// Parse the UTXO store URL
url, err := url.Parse(utxoStore)
require.NoError(t, err)
// Override the UTXO store setting
s.UtxoStore.UtxoStore = url
},
})
```
</details>

### Best Practices for Multi-Database Testing

1. **Always test all three backends** - SQLite for speed, PostgreSQL and Aerospike for production parity
2. **Use shared test functions** - Write test logic once, parameterize with `utxoStore`
3. **Handle cleanup properly** - Always defer teardown functions for containers
4. **Set SETTINGS_CONTEXT** - Add `os.Setenv("SETTINGS_CONTEXT", "test")` in init()
1. **Use UTXOStoreType option** - Always prefer the unified container management approach
2. **Test with two store types** - Aerospike and PostgreSQL
3. **Use shared test functions** - Write test logic once, parameterize with `storeType`
4. **Aerospike is default** - Most tests should use Aerospike as it's closest to production
5. **Use subtests** - Organize scenarios with `t.Run()` for better test output
6. **Consider test isolation** - Each database backend should be independent

Expand Down
Loading
Loading