Skip to content

Commit 0f3e368

Browse files
[CLD-518]: fix(anvil): fix nil pointer when T is not provided (#313)
Because anvil provider can be used outside of testing, the testing.T is not available, however currently , when it is not provided, the code returns nil pointer error during the call to testcontainers.CleanupContainer, this fix handles that scenario and provide a dedicated method for cleaning up container. ``` // Usage in production/non-test contexts with manual cleanup: // // func main() { // var once sync.Once // config := CTFAnvilChainProviderConfig{ // Once: &once, // ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute), // Port: "8545", // T not required when Port is provided // } // // provider := NewCTFAnvilChainProvider(chainSelector, config) // blockchain, err := provider.Initialize(context.Background()) // if err != nil { // log.Fatal(err) // } // // // Use the blockchain... // // // Important: Clean up the container when done // defer func() { // if err := provider.Cleanup(context.Background()); err != nil { // log.Printf("Failed to cleanup container: %v", err) // } // }() // } ``` When used in testing environment (passing in testing.T), container will be auto cleaned up. JIRA: https://smartcontract-it.atlassian.net/browse/CLD-518
1 parent 4ef3084 commit 0f3e368

File tree

3 files changed

+234
-10
lines changed

3 files changed

+234
-10
lines changed

.changeset/tricky-trams-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
fix(anvil): fix nil pointer when T is not provided

chain/evm/provider/ctf_anvil_provider.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@
148148
// // Chain ID is automatically derived from the chainSelector
149149
// }
150150
//
151+
// Usage in production/non-test contexts with manual cleanup:
152+
//
153+
// func main() {
154+
// var once sync.Once
155+
// config := CTFAnvilChainProviderConfig{
156+
// Once: &once,
157+
// ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
158+
// Port: "8545", // T not required when Port is provided
159+
// }
160+
//
161+
// provider := NewCTFAnvilChainProvider(chainSelector, config)
162+
// blockchain, err := provider.Initialize(context.Background())
163+
// if err != nil {
164+
// log.Fatal(err)
165+
// }
166+
//
167+
// // Use the blockchain...
168+
//
169+
// // Important: Clean up the container when done
170+
// defer func() {
171+
// if err := provider.Cleanup(context.Background()); err != nil {
172+
// log.Printf("Failed to cleanup container: %v", err)
173+
// }
174+
// }()
175+
// }
176+
//
151177
// # Chain Selectors
152178
//
153179
// Common chain selectors for Anvil testing:
@@ -308,8 +334,9 @@ type CTFAnvilChainProvider struct {
308334
selector uint64
309335
config CTFAnvilChainProviderConfig
310336

311-
chain *evm.Chain
312-
httpURL string
337+
chain *evm.Chain
338+
httpURL string
339+
container testcontainers.Container
313340
}
314341

315342
// NewCTFAnvilChainProvider creates a new CTFAnvilChainProvider with the given selector and
@@ -474,6 +501,28 @@ func (p *CTFAnvilChainProvider) GetNodeHTTPURL() string {
474501
return p.httpURL
475502
}
476503

504+
// Cleanup terminates the Anvil container and cleans up associated resources.
505+
//
506+
// This method provides explicit control over container lifecycle, which is especially
507+
// important when the provider is used outside of test contexts where automatic cleanup
508+
// via testcontainers.CleanupContainer is not available.
509+
//
510+
// It's safe to call this method multiple times - subsequent calls will be no-ops if
511+
// the container has already been terminated.
512+
//
513+
// Returns an error if the container termination fails.
514+
func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error {
515+
if p.container != nil {
516+
err := p.container.Terminate(ctx)
517+
if err != nil {
518+
return fmt.Errorf("failed to terminate Anvil container: %w", err)
519+
}
520+
p.container = nil // Clear the reference after successful termination
521+
}
522+
523+
return nil
524+
}
525+
477526
// startContainer starts a CTF container for the Anvil EVM returning the HTTP URL of the node.
478527
//
479528
// This method handles the Docker container lifecycle including:
@@ -534,7 +583,13 @@ func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID stri
534583
return "", fmt.Errorf("failed to create Anvil container: %w", rerr)
535584
}
536585

537-
testcontainers.CleanupContainer(p.config.T, output.Container)
586+
// Store container reference for manual cleanup
587+
p.container = output.Container
588+
589+
// Only register cleanup if T is available (for test cleanup)
590+
if p.config.T != nil {
591+
testcontainers.CleanupContainer(p.config.T, output.Container)
592+
}
538593

539594
return output.Nodes[0].ExternalHTTPUrl, nil
540595
},

chain/evm/provider/ctf_anvil_provider_test.go

Lines changed: 171 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package provider
22

33
import (
44
"context"
5+
"strconv"
56
"sync"
67
"testing"
78
"time"
89

10+
"github.com/smartcontractkit/freeport"
911
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113

@@ -33,7 +35,7 @@ func TestCTFAnvilChainProvider_Initialize(t *testing.T) {
3335
assert.Equal(t, selector, provider.ChainSelector())
3436
assert.Equal(t, "Anvil EVM CTF Chain Provider", provider.Name())
3537

36-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
38+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
3739
defer cancel()
3840

3941
// Initialize the provider
@@ -281,7 +283,7 @@ func TestCTFAnvilChainProvider_InitializeErrors(t *testing.T) {
281283

282284
provider := NewCTFAnvilChainProvider(tt.selector, tt.config)
283285

284-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
286+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
285287
defer cancel()
286288

287289
_, err := provider.Initialize(ctx)
@@ -311,7 +313,7 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
311313
selector := uint64(13264668187771770619) // Chain ID 31337
312314
provider := NewCTFAnvilChainProvider(selector, config)
313315

314-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
316+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
315317
defer cancel()
316318

317319
blockchain, err := provider.Initialize(ctx)
@@ -350,7 +352,7 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
350352
selector := uint64(13264668187771770619) // Chain ID 31337
351353
provider := NewCTFAnvilChainProvider(selector, config)
352354

353-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
355+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
354356
defer cancel()
355357

356358
blockchain, err := provider.Initialize(ctx)
@@ -388,7 +390,7 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
388390
selector := uint64(13264668187771770619) // Chain ID 31337
389391
provider := NewCTFAnvilChainProvider(selector, config)
390392

391-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
393+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
392394
defer cancel()
393395

394396
blockchain, err := provider.Initialize(ctx)
@@ -427,7 +429,7 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
427429
selector := uint64(13264668187771770619) // Chain ID 31337
428430
provider := NewCTFAnvilChainProvider(selector, config)
429431

430-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
432+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
431433
defer cancel()
432434

433435
blockchain, err := provider.Initialize(ctx)
@@ -462,7 +464,7 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
462464
selector := uint64(13264668187771770619) // Chain ID 31337
463465
provider := NewCTFAnvilChainProvider(selector, config)
464466

465-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
467+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
466468
defer cancel()
467469

468470
blockchain, err := provider.Initialize(ctx)
@@ -473,3 +475,165 @@ func TestCTFAnvilChainProvider_SignerIntegration(t *testing.T) {
473475
assert.True(t, clientOptsCalled, "ClientOpts should have been called during initialization")
474476
})
475477
}
478+
479+
func TestCTFAnvilChainProvider_Cleanup(t *testing.T) {
480+
t.Parallel()
481+
482+
t.Run("cleanup after initialization with T provided", func(t *testing.T) {
483+
t.Parallel()
484+
485+
var once sync.Once
486+
config := CTFAnvilChainProviderConfig{
487+
Once: &once,
488+
ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
489+
T: t,
490+
}
491+
492+
selector := uint64(13264668187771770619) // Chain ID 31337
493+
provider := NewCTFAnvilChainProvider(selector, config)
494+
495+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
496+
defer cancel()
497+
498+
blockchain, err := provider.Initialize(ctx)
499+
require.NoError(t, err)
500+
require.NotNil(t, blockchain)
501+
502+
assert.NotNil(t, provider.container, "Container reference should be stored after initialization")
503+
504+
err = provider.Cleanup(ctx)
505+
require.NoError(t, err, "Cleanup should succeed")
506+
507+
assert.Nil(t, provider.container, "Container reference should be cleared after cleanup")
508+
})
509+
510+
t.Run("cleanup after initialization with fixed port (T=nil)", func(t *testing.T) {
511+
t.Parallel()
512+
513+
// Allocate a free port for this test
514+
port := freeport.GetOne(t)
515+
defer freeport.Return([]int{port})
516+
517+
var once sync.Once
518+
config := CTFAnvilChainProviderConfig{
519+
Once: &once,
520+
ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
521+
Port: strconv.Itoa(port), // Use allocated port to avoid conflicts
522+
T: nil, // No T provided - simulates production usage
523+
}
524+
525+
selector := uint64(13264668187771770619) // Chain ID 31337
526+
provider := NewCTFAnvilChainProvider(selector, config)
527+
528+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
529+
defer cancel()
530+
531+
blockchain, err := provider.Initialize(ctx)
532+
require.NoError(t, err)
533+
require.NotNil(t, blockchain)
534+
535+
assert.NotNil(t, provider.container, "Container reference should be stored after initialization")
536+
537+
err = provider.Cleanup(ctx)
538+
require.NoError(t, err, "Cleanup should succeed even when T is nil")
539+
540+
assert.Nil(t, provider.container, "Container reference should be cleared after cleanup")
541+
})
542+
543+
t.Run("cleanup before initialization", func(t *testing.T) {
544+
t.Parallel()
545+
546+
var once sync.Once
547+
config := CTFAnvilChainProviderConfig{
548+
Once: &once,
549+
ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
550+
T: t,
551+
}
552+
553+
selector := uint64(13264668187771770619) // Chain ID 31337
554+
provider := NewCTFAnvilChainProvider(selector, config)
555+
556+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
557+
defer cancel()
558+
559+
// Test cleanup before initialization - should be a no-op
560+
err := provider.Cleanup(ctx)
561+
require.NoError(t, err, "Cleanup should succeed even when no container exists")
562+
563+
// Verify container is still nil
564+
assert.Nil(t, provider.container, "Container reference should remain nil")
565+
})
566+
567+
t.Run("multiple cleanup calls", func(t *testing.T) {
568+
t.Parallel()
569+
570+
var once sync.Once
571+
config := CTFAnvilChainProviderConfig{
572+
Once: &once,
573+
ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
574+
T: t,
575+
}
576+
577+
selector := uint64(13264668187771770619) // Chain ID 31337
578+
provider := NewCTFAnvilChainProvider(selector, config)
579+
580+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
581+
defer cancel()
582+
583+
blockchain, err := provider.Initialize(ctx)
584+
require.NoError(t, err)
585+
require.NotNil(t, blockchain)
586+
587+
// First cleanup
588+
err = provider.Cleanup(ctx)
589+
require.NoError(t, err, "First cleanup should succeed")
590+
assert.Nil(t, provider.container, "Container reference should be cleared after first cleanup")
591+
592+
// Second cleanup - should be a no-op
593+
err = provider.Cleanup(ctx)
594+
require.NoError(t, err, "Second cleanup should succeed (no-op)")
595+
assert.Nil(t, provider.container, "Container reference should remain nil after second cleanup")
596+
597+
// Third cleanup - should still be a no-op
598+
err = provider.Cleanup(ctx)
599+
require.NoError(t, err, "Third cleanup should succeed (no-op)")
600+
assert.Nil(t, provider.container, "Container reference should remain nil after third cleanup")
601+
})
602+
603+
t.Run("cleanup with cancelled context", func(t *testing.T) {
604+
t.Parallel()
605+
606+
var once sync.Once
607+
config := CTFAnvilChainProviderConfig{
608+
Once: &once,
609+
ConfirmFunctor: ConfirmFuncGeth(2 * time.Minute),
610+
T: t,
611+
}
612+
613+
selector := uint64(13264668187771770619) // Chain ID 31337
614+
provider := NewCTFAnvilChainProvider(selector, config)
615+
616+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
617+
defer cancel()
618+
619+
blockchain, err := provider.Initialize(ctx)
620+
require.NoError(t, err)
621+
require.NotNil(t, blockchain)
622+
623+
cancelledCtx, cancelFunc := context.WithCancel(t.Context())
624+
cancelFunc() // Cancel immediately
625+
626+
// Test cleanup with cancelled context - should return an error
627+
err = provider.Cleanup(cancelledCtx)
628+
require.Error(t, err, "Cleanup should fail with cancelled context")
629+
assert.Contains(t, err.Error(), "failed to terminate Anvil container", "Error should mention container termination failure")
630+
631+
// Container reference should still exist since cleanup failed
632+
assert.NotNil(t, provider.container, "Container reference should remain when cleanup fails")
633+
634+
// Cleanup with proper context should still work
635+
err = provider.Cleanup(ctx)
636+
require.NoError(t, err, "Cleanup with valid context should succeed after previous failure")
637+
assert.Nil(t, provider.container, "Container reference should be cleared after successful cleanup")
638+
})
639+
}

0 commit comments

Comments
 (0)