Skip to content

Commit e07b5ca

Browse files
committed
Speed up Solana integration tests by avoiding redundant container setup.
Cache MCMS program artifacts once per process, fix PreloadMCMS nested-subtest deadlocks, use a mock env for set-config VerifyPreconditions, and reuse an existing chain in fund-mcm-pdas instead of starting a second validator.
1 parent 628cfb7 commit e07b5ca

5 files changed

Lines changed: 114 additions & 86 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: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package soltestutils
22

33
import (
4-
"io"
5-
"os"
64
"path/filepath"
5+
"sync"
76
"testing"
87

98
"github.com/stretchr/testify/require"
@@ -13,71 +12,82 @@ import (
1312
"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
1413
)
1514

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()
15+
// solTestExclusive serializes top-level Solana integration tests that mutate global
16+
// gobinding program IDs via SetProgramID. solTestDepth allows nested subtests in
17+
// the same test to re-enter without deadlocking.
18+
var (
19+
solTestExclusive sync.Mutex
20+
solTestCountMu sync.Mutex
21+
solTestDepth int
22+
)
2123

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

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

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) {
32-
t.Helper()
45+
var (
46+
mcmsProgramsOnce sync.Once
47+
mcmsProgramsPath string
48+
mcmsProgramIDs map[string]string
49+
)
3350

34-
dir := t.TempDir()
51+
// sharedMCMSPrograms downloads MCMS Solana program artifacts once per test process
52+
// and returns the shared cache directory plus program IDs.
53+
func sharedMCMSPrograms(t *testing.T) (string, map[string]string) {
54+
t.Helper()
3555

36-
_, programIDs := LoadMCMSPrograms(t, dir)
56+
mcmsProgramsOnce.Do(func() {
57+
mcmsProgramsPath = programsCacheDir()
58+
err := solutils.DownloadChainlinkCCIPProgramArtifacts(t.Context(), mcmsProgramsPath, "", nil)
59+
require.NoError(t, err)
60+
61+
mcmsProgramIDs = make(map[string]string, len(solutils.MCMSProgramNames))
62+
for _, name := range solutils.MCMSProgramNames {
63+
id := solutils.GetProgramID(name)
64+
require.NotEmpty(t, id, "program id not found for program name: %s", name)
65+
require.FileExists(t, filepath.Join(mcmsProgramsPath, name+".so"))
66+
mcmsProgramIDs[name] = id
67+
}
68+
})
69+
70+
return mcmsProgramsPath, copyProgramIDs(mcmsProgramIDs)
71+
}
3772

38-
ab := PreloadAddressBookWithMCMSPrograms(t, selector)
73+
func copyProgramIDs(src map[string]string) map[string]string {
74+
dst := make(map[string]string, len(src))
75+
for name, id := range src {
76+
dst[name] = id
77+
}
3978

40-
return dir, programIDs, ab
79+
return dst
4180
}
4281

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 {
82+
// PreloadMCMS provides a convenience function to preload the MCMS program artifacts and address
83+
// book for a given selector.
84+
func PreloadMCMS(t *testing.T, selector uint64) (string, map[string]string, *cldf.AddressBookMap) {
4885
t.Helper()
4986

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)
87+
acquireSolanaTestIsolation(t)
5988

60-
src := filepath.Join(cachePath, name+".so")
61-
dst := filepath.Join(targetDir, name+".so")
62-
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-
}
89+
programsPath, programIDs := sharedMCMSPrograms(t)
90+
ab := PreloadAddressBookWithMCMSPrograms(t, selector)
8091

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

mcms/changesets/set-config/changeset_test.go

Lines changed: 3 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)

mcms/changesets/set-config/helpers_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
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"
17+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
1318
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
1419
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
1520
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
@@ -146,6 +151,42 @@ func evmMCMSChainState(t *testing.T, rt *runtime.Runtime, selector uint64) (*evm
146151
return mcmsState, chain
147152
}
148153

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

0 commit comments

Comments
 (0)