Skip to content

Commit d52089d

Browse files
authored
fix: improve solana speed tests (#111)
Reuse some of the containers to speed up tests for solana
1 parent 628cfb7 commit d52089d

7 files changed

Lines changed: 273 additions & 227 deletions

File tree

legacy/pkg/family/solana/testutils/artifacts.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,8 @@ package soltestutils
33
import (
44
"os"
55
"path/filepath"
6-
"sync"
7-
"testing"
8-
9-
"github.com/stretchr/testify/require"
10-
11-
"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
126
)
137

14-
var (
15-
onceCCIP = &sync.Once{}
16-
)
17-
18-
type downloadFunc func(t *testing.T) string
19-
20-
// downloadChainlinkCCIPProgramArtifacts downloads CCIP Solana artifacts (includes MCMS programs).
21-
func downloadChainlinkCCIPProgramArtifacts(t *testing.T) string {
22-
t.Helper()
23-
24-
cachePath := programsCacheDir()
25-
26-
onceCCIP.Do(func() {
27-
err := solutils.DownloadChainlinkCCIPProgramArtifacts(t.Context(), cachePath, "", nil)
28-
require.NoError(t, err)
29-
})
30-
31-
return cachePath
32-
}
33-
348
// programsCacheDir returns where to store downloaded .so files. Leaf dir is solana_programs
359
// (under UserCacheDir/TempDir, so "cache" is implied; avoids read-only pkg/mod paths).
3610
func programsCacheDir() string {
Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package soltestutils
22

33
import (
4-
"io"
5-
"os"
4+
"maps"
65
"path/filepath"
6+
"sync"
77
"testing"
88

99
"github.com/stretchr/testify/require"
@@ -13,71 +13,76 @@ import (
1313
"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
1414
)
1515

16-
// LoadMCMSPrograms loads the MCMS program artifacts into the given directory.
17-
//
18-
// Returns the path to the temporary test directory and a map of program names to IDs.
19-
func LoadMCMSPrograms(t *testing.T, dir string) (string, map[string]string) {
20-
t.Helper()
16+
// solTestExclusive serializes top-level Solana integration tests that mutate global
17+
// gobinding program IDs via SetProgramID. solTestDepth allows nested subtests in
18+
// the same test to re-enter without deadlocking.
19+
var (
20+
solTestExclusive sync.Mutex
21+
solTestCountMu sync.Mutex
22+
solTestDepth int
23+
)
2124

22-
progIDs := loadProgramArtifacts(t,
23-
solutils.MCMSProgramNames, downloadChainlinkCCIPProgramArtifacts, dir,
24-
)
25+
func acquireSolanaTestIsolation(t *testing.T) {
26+
t.Helper()
2527

26-
return dir, progIDs
28+
solTestCountMu.Lock()
29+
if solTestDepth == 0 {
30+
solTestExclusive.Lock()
31+
}
32+
solTestDepth++
33+
solTestCountMu.Unlock()
34+
35+
t.Cleanup(func() {
36+
solTestCountMu.Lock()
37+
solTestDepth--
38+
release := solTestDepth == 0
39+
solTestCountMu.Unlock()
40+
if release {
41+
solTestExclusive.Unlock()
42+
}
43+
})
2744
}
2845

29-
// PreloadMCMS provides a convenience function to preload the MCMS program artifacts and address
30-
// book for a given selector.
31-
func PreloadMCMS(t *testing.T, selector uint64) (string, map[string]string, *cldf.AddressBookMap) {
46+
var (
47+
mcmsProgramsOnce sync.Once
48+
mcmsProgramsPath string
49+
mcmsProgramIDs map[string]string
50+
)
51+
52+
// sharedMCMSPrograms downloads MCMS Solana program artifacts once per test process
53+
// and returns the shared cache directory plus program IDs.
54+
func sharedMCMSPrograms(t *testing.T) (string, map[string]string) {
3255
t.Helper()
3356

34-
dir := t.TempDir()
57+
mcmsProgramsOnce.Do(func() {
58+
mcmsProgramsPath = programsCacheDir()
59+
err := solutils.DownloadChainlinkCCIPProgramArtifacts(t.Context(), mcmsProgramsPath, "", nil)
60+
require.NoError(t, err)
3561

36-
_, programIDs := LoadMCMSPrograms(t, dir)
62+
mcmsProgramIDs = make(map[string]string, len(solutils.MCMSProgramNames))
63+
for _, name := range solutils.MCMSProgramNames {
64+
id := solutils.GetProgramID(name)
65+
require.NotEmpty(t, id, "program id not found for program name: %s", name)
66+
require.FileExists(t, filepath.Join(mcmsProgramsPath, name+".so"))
67+
mcmsProgramIDs[name] = id
68+
}
69+
})
3770

38-
ab := PreloadAddressBookWithMCMSPrograms(t, selector)
71+
programIDs := make(map[string]string, len(mcmsProgramIDs))
72+
maps.Copy(programIDs, mcmsProgramIDs)
3973

40-
return dir, programIDs, ab
74+
return mcmsProgramsPath, programIDs
4175
}
4276

43-
// loadProgramArtifacts is a helper function that loads program artifacts into a temporary test directory.
44-
// It downloads artifacts using the provided download function and copies the specified programs.
45-
//
46-
// Returns the map of program names to IDs.
47-
func loadProgramArtifacts(t *testing.T, programNames []string, downloadFn downloadFunc, targetDir string) map[string]string {
77+
// PreloadMCMS provides a convenience function to preload the MCMS program artifacts and address
78+
// book for a given selector.
79+
func PreloadMCMS(t *testing.T, selector uint64) (string, map[string]string, *cldf.AddressBookMap) {
4880
t.Helper()
4981

50-
// Download the program artifacts using the provided download function
51-
cachePath := downloadFn(t)
52-
53-
progIDs := make(map[string]string, len(programNames))
54-
55-
// Copy the specific artifacts to the target directory and add the program ID to the map
56-
for _, name := range programNames {
57-
id := solutils.GetProgramID(name)
58-
require.NotEmpty(t, id, "program id not found for program name: %s", name)
59-
60-
src := filepath.Join(cachePath, name+".so")
61-
dst := filepath.Join(targetDir, name+".so")
82+
acquireSolanaTestIsolation(t)
6283

63-
func() {
64-
srcFile, err := os.Open(src)
65-
require.NoError(t, err)
66-
defer srcFile.Close()
67-
68-
dstFile, err := os.Create(dst)
69-
require.NoError(t, err)
70-
defer dstFile.Close()
71-
72-
_, err = io.Copy(dstFile, srcFile)
73-
require.NoError(t, err)
74-
}()
75-
76-
// Add the program ID to the map
77-
progIDs[name] = id
78-
t.Logf("copied solana program %s to %s", name, dst)
79-
}
84+
programsPath, programIDs := sharedMCMSPrograms(t)
85+
ab := PreloadAddressBookWithMCMSPrograms(t, selector)
8086

81-
// Return the path to the cached artifacts and the map of program IDs
82-
return progIDs
87+
return programsPath, programIDs, ab
8388
}

mcms/changesets/set-config/changeset_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ func TestChangeset_VerifyPreconditions(t *testing.T) {
176176
}
177177
}
178178

179-
//nolint:paralleltest // global mcm.SetProgramID state and shared Solana CTF container setup
180179
func TestChangeset_VerifyPreconditions_Solana(t *testing.T) {
180+
t.Parallel()
181+
181182
selector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector
182-
rt := newSolanaRuntimeWithDeploy(t, selector)
183-
env := rt.Environment()
183+
env := newSolanaVerifyPreconditionsEnv(t, selector)
184184

185185
validCfg := cldftesthelpers.SingleGroupMCMS(t)
186186
validTargets := mcmsTargets(selector, validCfg, validCfg, validCfg)
@@ -200,6 +200,7 @@ func TestChangeset_VerifyPreconditions_Solana(t *testing.T) {
200200
},
201201
} {
202202
t.Run(tt.name, func(t *testing.T) {
203+
t.Parallel()
203204
require.NoError(t, cs.VerifyPreconditions(env, tt.input))
204205
})
205206
}

mcms/changesets/set-config/helpers_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package setconfig_test
22

33
import (
4+
"context"
45
"crypto/ecdsa"
56
"testing"
67
"time"
78

9+
"github.com/Masterminds/semver/v3"
810
"github.com/ethereum/go-ethereum/common"
911
"github.com/stretchr/testify/require"
1012

13+
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
1114
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
15+
cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
1216
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
1317
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
1418
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
@@ -146,6 +150,42 @@ func evmMCMSChainState(t *testing.T, rt *runtime.Runtime, selector uint64) (*evm
146150
return mcmsState, chain
147151
}
148152

153+
// newSolanaVerifyPreconditionsEnv builds a mock Solana environment for VerifyPreconditions
154+
// only — no CTF container or on-chain deploy.
155+
func newSolanaVerifyPreconditionsEnv(t *testing.T, selector uint64) cldf.Environment {
156+
t.Helper()
157+
158+
ds := cldfdatastore.NewMemoryDataStore()
159+
version := semver.MustParse("1.0.0")
160+
for _, ref := range []struct {
161+
contractType cldf.ContractType
162+
address string
163+
}{
164+
{mcmscontracts.RBACTimelock, "timelock-address"},
165+
{mcmscontracts.ProposerManyChainMultisig, "proposer-address"},
166+
{mcmscontracts.CancellerManyChainMultisig, "canceller-address"},
167+
{mcmscontracts.BypasserManyChainMultisig, "bypasser-address"},
168+
} {
169+
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
170+
Address: ref.address,
171+
ChainSelector: selector,
172+
Type: cldfdatastore.ContractType(ref.contractType),
173+
Version: version,
174+
}))
175+
}
176+
177+
return cldf.Environment{
178+
Logger: logger.Test(t),
179+
DataStore: ds.Seal(),
180+
GetContext: func() context.Context {
181+
return t.Context()
182+
},
183+
BlockChains: cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
184+
selector: cldfsol.Chain{Selector: selector},
185+
}),
186+
}
187+
}
188+
149189
func newSolanaRuntimeWithDeploy(t *testing.T, selector uint64) *runtime.Runtime {
150190
t.Helper()
151191

mcms/solana/changesets/fund-mcm-pdas/changeset_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestChangeset(t *testing.T) {
3333
selector2 := chainselectors.TEST_33333333333333333333333333333333333333333333.Selector
3434

3535
rt1 := testRuntime(t, selector1)
36-
env1 := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000), true)
36+
env1 := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000))
3737
cs := Changeset{}
3838

3939
t.Run("VerifyPreconditions", func(t *testing.T) {
@@ -92,7 +92,7 @@ func TestChangeset(t *testing.T) {
9292
},
9393
{
9494
name: "insufficient deployer balance",
95-
env: configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1), true),
95+
env: configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1)),
9696
config: Config{
9797
FundingPerChain: map[uint64]FundingConfig{selector1: {
9898
ProposeMCM: 100,
@@ -106,7 +106,7 @@ func TestChangeset(t *testing.T) {
106106
{
107107
name: "missing deployer key",
108108
env: func() cldf.Environment {
109-
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000), true)
109+
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, rpcWithBalance(t, 1_000))
110110
chain := env.BlockChains.SolanaChains()[selector1]
111111
chain.DeployerKey = nil
112112
env.BlockChains = cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{selector1: chain})
@@ -138,8 +138,11 @@ func TestChangeset(t *testing.T) {
138138
}
139139

140140
t.Run("mcms contracts not deployed", func(t *testing.T) {
141-
rt2 := testRuntime(t, selector2)
142-
env := configureFundMCMSignersEnv(t, rt2.Environment(), selector2, rpcWithBalance(t, 1_000), false)
141+
chain := rt1.Environment().BlockChains.SolanaChains()[selector1]
142+
chain.Client = rpcWithBalance(t, 1_000)
143+
env := cldf.Environment{DataStore: newMCMSDataStore(t, selector2, false)}
144+
env.BlockChains = cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{selector2: chain})
145+
143146
err := cs.VerifyPreconditions(env, Config{
144147
FundingPerChain: map[uint64]FundingConfig{selector2: {
145148
ProposeMCM: 100,
@@ -155,7 +158,7 @@ func TestChangeset(t *testing.T) {
155158
t.Run("Apply", func(t *testing.T) {
156159
var confirmed [][]solana.Instruction
157160

158-
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, nil, true)
161+
env := configureFundMCMSignersEnv(t, rt1.Environment(), selector1, nil)
159162
chain := env.BlockChains.SolanaChains()[selector1]
160163
require.NotNil(t, chain.DeployerKey)
161164
deployerKey := *chain.DeployerKey
@@ -246,12 +249,11 @@ func configureFundMCMSignersEnv(
246249
base cldf.Environment,
247250
selector uint64,
248251
client *rpc.Client,
249-
completeState bool,
250252
) cldf.Environment {
251253
t.Helper()
252254

253255
env := base
254-
env.DataStore = newMCMSDataStore(t, selector, completeState)
256+
env.DataStore = newMCMSDataStore(t, selector, true)
255257

256258
chain := env.BlockChains.SolanaChains()[selector]
257259
if client != nil {

0 commit comments

Comments
 (0)