Skip to content

Commit 50c3cd5

Browse files
authored
[DX-3386] parallelize CRE system-tests vol.1 (#21510)
* parallelize CRE system-tests * CR changes * add mutex to workflow deletion to avoid EVM error ReentrancySentryOOG * add a log, when expected user log is found, use zerolog.Logger in verifyTriggerEventACKs() * fix lint * parallelize CRE regression tests
1 parent 9db246c commit 50c3cd5

28 files changed

Lines changed: 1255 additions & 357 deletions

.github/workflows/cre-regression-system-tests.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ jobs:
183183
env:
184184
TEST_NAME: ${{ matrix.tests.test_name }}
185185
TEST_TIMEOUT: 30m
186+
PARALLEL_COUNT: "10"
187+
# parallelisation flags for tests
188+
# fanout is necessary, because multiple tests would otherwise start chip test sinks at the same port, causing conflicts
189+
CRE_TEST_PARALLEL_ENABLED: "true"
190+
CRE_TEST_CHIP_SINK_FANOUT_ENABLED: "true"
186191
run: |
187192
echo "Starting test: '${TEST_NAME}'"
188193
echo "⚠️⚠️⚠️ Add 'skip-e2e-regression' label to skip this step if necessary ⚠️⚠️⚠️"
@@ -192,7 +197,7 @@ jobs:
192197
--junitfile=/tmp/junit-report-regression.xml \
193198
--format=github-actions \
194199
-- \
195-
-v -run "^(${TEST_NAME})$" -timeout ${TEST_TIMEOUT} -count=1 -parallel=1 \
200+
-v -run "^(${TEST_NAME})$" -timeout ${TEST_TIMEOUT} -count=1 -parallel=${PARALLEL_COUNT} \
196201
github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre
197202
198203
echo "⚠️⚠️⚠️ Add 'skip-e2e-regression' label to skip this step if necessary ⚠️⚠️⚠️"

.github/workflows/cre-system-tests.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,14 +299,19 @@ jobs:
299299
RUN_QUARANTINED_TESTS: "true" # always run quarantined tests in CI
300300
TOPOLOGY_NAME: ${{ matrix.tests.topology }}
301301
GITHUB_TOKEN: ${{ steps.github-token.outputs.access-token || '' }} # to avoid rate limiting when downloading protobuf files from GitHub
302+
PARALLEL_COUNT: "10"
303+
# parallelisation flags for tests
304+
# fanout is necessary, because multiple tests would otherwise start chip test sinks at the same port, causing conflicts
305+
CRE_TEST_PARALLEL_ENABLED: "true"
306+
CRE_TEST_CHIP_SINK_FANOUT_ENABLED: "true"
302307
run: |
303308
echo "Starting test: '${TEST_NAME}'"
304309
gotestsum \
305310
--jsonfile=/tmp/gotest.log \
306311
--junitfile=/tmp/junit-report.xml \
307312
--format=github-actions \
308313
-- \
309-
-v -run "^(${TEST_NAME})$" -timeout "${TEST_TIMEOUT}" -count=1 -parallel=1 \
314+
-v -run "^(${TEST_NAME})$" -timeout "${TEST_TIMEOUT}" -count=1 -parallel="${PARALLEL_COUNT}" \
310315
github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre
311316
312317
exit_code=$?

system-tests/lib/cre/environment/blockchains/evm/evm.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ type Blockchain struct {
4747
SethClient *seth.Client
4848
}
4949

50+
// CloneWithSethClient returns a copy of the blockchain handle with a different Seth client.
51+
// This lets tests use per-test keys while preserving immutable chain metadata.
52+
func (e *Blockchain) CloneWithSethClient(sc *seth.Client) *Blockchain {
53+
return &Blockchain{
54+
testLogger: e.testLogger,
55+
chainSelector: e.chainSelector,
56+
chainID: e.chainID,
57+
ctfOutput: e.ctfOutput,
58+
SethClient: sc,
59+
}
60+
}
61+
62+
func (e *Blockchain) WSURL() string {
63+
if len(e.ctfOutput.Nodes) == 0 {
64+
return ""
65+
}
66+
return e.ctfOutput.Nodes[0].ExternalWSUrl
67+
}
68+
5069
func (e *Blockchain) ChainSelector() uint64 {
5170
return e.chainSelector
5271
}

system-tests/lib/cre/features/solana/v2/solana.go

Lines changed: 106 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"slices"
88
"strconv"
9+
"sync"
910
"text/template"
1011
"time"
1112

@@ -15,6 +16,7 @@ import (
1516
"github.com/pkg/errors"
1617
"github.com/rs/zerolog"
1718
chainselectors "github.com/smartcontractkit/chain-selectors"
19+
"golang.org/x/sync/errgroup"
1820
"google.golang.org/protobuf/types/known/durationpb"
1921

2022
capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
@@ -174,106 +176,122 @@ func createJobs(
174176
return errors.Wrapf(chErr, "failed to get Solana chain ID from selector %d", solChain.ChainSelector())
175177
}
176178

179+
solChainID, err := solChain.SolClient.GetGenesisHash(ctx)
180+
if err != nil {
181+
return errors.Wrapf(err, "failed to get sol genesis hash")
182+
}
183+
version := creEnv.ContractVersions[cre_sol.ForwarderContract.String()]
184+
creForwarderKey := datastore.NewAddressRefKey(
185+
solChain.ChainSelector(),
186+
cre_sol.ForwarderContract,
187+
version,
188+
cre_sol.DefaultForwarderQualifier,
189+
)
190+
creForwarderStateKey := datastore.NewAddressRefKey(
191+
solChain.ChainSelector(),
192+
cre_sol.ForwarderState,
193+
version,
194+
cre_sol.DefaultForwarderQualifier,
195+
)
196+
creForwarderAddress, err := creEnv.CldfEnvironment.DataStore.Addresses().Get(creForwarderKey)
197+
if err != nil {
198+
return errors.Wrap(err, "failed to get CRE Forwarder address")
199+
}
200+
creForwarderStateAddress, err := creEnv.CldfEnvironment.DataStore.Addresses().Get(creForwarderStateKey)
201+
if err != nil {
202+
return errors.Wrap(err, "failed to get CRE Forwarder State address")
203+
}
204+
tmpl, err := template.New("solConfig").Parse(configTemplate)
205+
if err != nil {
206+
return errors.Wrapf(err, "failed to parse %s config template", flag)
207+
}
208+
209+
var specsMu sync.Mutex
210+
group, groupCtx := errgroup.WithContext(ctx)
177211
for _, workerNode := range workerNodes {
178-
key, ok := workerNode.Keys.Solana[chainID]
179-
if !ok {
180-
return fmt.Errorf("failed to get solana key (chainID %s, node index %d)", chainID, workerNode.Index)
181-
}
212+
group.Go(func() error {
213+
key, ok := workerNode.Keys.Solana[chainID]
214+
if !ok {
215+
return fmt.Errorf("failed to get solana key (chainID %s, node index %d)", chainID, workerNode.Index)
216+
}
182217

183-
version := creEnv.ContractVersions[cre_sol.ForwarderContract.String()]
218+
nodeAddress := key.PublicAddress.String()
219+
runtimeFallbacks := map[string]any{
220+
"CREForwarderAddress": creForwarderAddress.Address,
221+
"CREForwarderState": creForwarderStateAddress.Address,
222+
"NodeAddress": nodeAddress,
223+
"IsLocal": true,
224+
"Network": "solana",
225+
"ChainID": solChainID.String(),
226+
}
184227

185-
creForwarderKey := datastore.NewAddressRefKey(
186-
solChain.ChainSelector(),
187-
cre_sol.ForwarderContract,
188-
version,
189-
cre_sol.DefaultForwarderQualifier,
190-
)
191-
creForwarderStateKey := datastore.NewAddressRefKey(
192-
solChain.ChainSelector(),
193-
cre_sol.ForwarderState,
194-
version,
195-
cre_sol.DefaultForwarderQualifier,
196-
)
197-
creForwarderAddress, err := creEnv.CldfEnvironment.DataStore.Addresses().Get(creForwarderKey)
198-
if err != nil {
199-
return errors.Wrap(err, "failed to get CRE Forwarder address")
200-
}
201-
creForwarderStateAddress, err := creEnv.CldfEnvironment.DataStore.Addresses().Get(creForwarderStateKey)
202-
if err != nil {
203-
return errors.Wrap(err, "failed to get CRE Forwarder State address")
204-
}
228+
templateData, aErr := credon.ApplyRuntimeValues(config.Values, runtimeFallbacks)
229+
if aErr != nil {
230+
return errors.Wrap(aErr, "failed to apply runtime values")
231+
}
205232

206-
nodeAddress := key.PublicAddress.String()
207-
tmpl, err := template.New("solConfig").Parse(configTemplate)
208-
if err != nil {
209-
return errors.Wrapf(err, "failed to parse %s config template", flag)
210-
}
233+
var configBuffer bytes.Buffer
234+
if err := tmpl.Execute(&configBuffer, templateData); err != nil {
235+
return errors.Wrapf(err, "failed to execute %s config template", flag)
236+
}
211237

212-
solChainID, err := solChain.SolClient.GetGenesisHash(ctx)
213-
if err != nil {
214-
return errors.Wrapf(err, "failed to get sol genesis hash")
215-
}
216-
runtimeFallbacks := map[string]any{
217-
"CREForwarderAddress": creForwarderAddress.Address,
218-
"CREForwarderState": creForwarderStateAddress.Address,
219-
"NodeAddress": nodeAddress,
220-
"IsLocal": true,
221-
"Network": "solana",
222-
"ChainID": solChainID.String(),
223-
}
238+
configStr := configBuffer.String()
239+
if err := credon.ValidateTemplateSubstitution(configStr, flag); err != nil {
240+
return errors.Wrapf(err, "%s template validation failed", flag)
241+
}
224242

225-
templateData, aErr := credon.ApplyRuntimeValues(config.Values, runtimeFallbacks)
226-
if aErr != nil {
227-
return errors.Wrap(aErr, "failed to apply runtime values")
228-
}
243+
workerInput := cre_jobs.ProposeJobSpecInput{
244+
Domain: offchain.ProductLabel,
245+
Environment: cre.EnvironmentName,
246+
DONName: don.Name,
247+
JobName: "sol-v2-worker-" + chainID,
248+
ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag},
249+
DONFilters: []offchain.TargetDONFilter{
250+
{Key: offchain.FilterKeyDONName, Value: don.Name},
251+
{Key: "p2p_id", Value: workerNode.Keys.PeerID()}, // required since each node requires a different config (it contains its own from address)
252+
},
253+
Template: job_types.Solana,
254+
Inputs: job_types.JobSpecInput{
255+
"command": command,
256+
"config": configStr,
257+
},
258+
}
229259

230-
var configBuffer bytes.Buffer
231-
if err := tmpl.Execute(&configBuffer, templateData); err != nil {
232-
return errors.Wrapf(err, "failed to execute %s config template", flag)
233-
}
260+
workerVerErr := cre_jobs.ProposeJobSpec{}.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput)
261+
if workerVerErr != nil {
262+
return fmt.Errorf("precondition verification failed for Solana v2 worker job: %w", workerVerErr)
263+
}
234264

235-
configStr := configBuffer.String()
236-
if err := credon.ValidateTemplateSubstitution(configStr, flag); err != nil {
237-
return errors.Wrapf(err, "%s template validation failed", flag)
238-
}
265+
workerReport, workerErr := cre_jobs.ProposeJobSpec{}.Apply(*creEnv.CldfEnvironment, workerInput)
266+
if workerErr != nil {
267+
return fmt.Errorf("failed to propose Solana v2 worker job spec: %w", workerErr)
268+
}
239269

240-
workerInput := cre_jobs.ProposeJobSpecInput{
241-
Domain: offchain.ProductLabel,
242-
Environment: cre.EnvironmentName,
243-
DONName: don.Name,
244-
JobName: "sol-v2-worker-" + chainID,
245-
ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag},
246-
DONFilters: []offchain.TargetDONFilter{
247-
{Key: offchain.FilterKeyDONName, Value: don.Name},
248-
{Key: "p2p_id", Value: workerNode.Keys.PeerID()}, // required since each node requires a different config (it contains its own from address)
249-
},
250-
Template: job_types.Solana,
251-
Inputs: job_types.JobSpecInput{
252-
"command": command,
253-
"config": configStr,
254-
},
255-
}
270+
specsMu.Lock()
271+
defer specsMu.Unlock()
272+
for _, r := range workerReport.Reports {
273+
out, ok := r.Output.(cre_jobs_ops.ProposeStandardCapabilityJobOutput)
274+
if !ok {
275+
return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", r.Output)
276+
}
277+
mErr := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice)
278+
if mErr != nil {
279+
return fmt.Errorf("failed to merge worker job specs: %w", mErr)
280+
}
281+
}
256282

257-
workerVerErr := cre_jobs.ProposeJobSpec{}.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput)
258-
if workerVerErr != nil {
259-
return fmt.Errorf("precondition verification failed for Solana v2 worker job: %w", workerVerErr)
260-
}
283+
select {
284+
case <-groupCtx.Done():
285+
return groupCtx.Err()
286+
default:
287+
}
261288

262-
workerReport, workerErr := cre_jobs.ProposeJobSpec{}.Apply(*creEnv.CldfEnvironment, workerInput)
263-
if workerErr != nil {
264-
return fmt.Errorf("failed to propose Solana v2 worker job spec: %w", workerErr)
265-
}
289+
return nil
290+
})
291+
}
266292

267-
for _, r := range workerReport.Reports {
268-
out, ok := r.Output.(cre_jobs_ops.ProposeStandardCapabilityJobOutput)
269-
if !ok {
270-
return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", r.Output)
271-
}
272-
mErr := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice)
273-
if mErr != nil {
274-
return fmt.Errorf("failed to merge worker job specs: %w", mErr)
275-
}
276-
}
293+
if err := group.Wait(); err != nil {
294+
return err
277295
}
278296

279297
approveErr := jobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs)

system-tests/lib/cre/workflow/compile.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ const (
2828
// It will return an error if the workflow name is less than 10 characters long.
2929
// It will return an error if the workflow file path is not a valid file path.
3030
func CompileWorkflow(ctx context.Context, workflowFilePath, workflowName string) (string, error) {
31+
return CompileWorkflowToDir(ctx, workflowFilePath, workflowName, "")
32+
}
33+
34+
// CompileWorkflowToDir compiles a workflow and stores build artifacts in outputDir.
35+
// If outputDir is empty, a temporary directory is created automatically.
36+
func CompileWorkflowToDir(ctx context.Context, workflowFilePath, workflowName, outputDir string) (string, error) {
3137
if len(workflowName) < 10 {
3238
return "", errors.New("workflow name must be at least 10 characters long")
3339
}
40+
if outputDir == "" {
41+
var err error
42+
outputDir, err = os.MkdirTemp("", "cre-workflow-build-*")
43+
if err != nil {
44+
return "", errors.Wrap(err, "failed to create temporary workflow build dir")
45+
}
46+
}
47+
if mkErr := os.MkdirAll(outputDir, 0o755); mkErr != nil {
48+
return "", errors.Wrap(mkErr, "failed to prepare workflow build dir")
49+
}
3450

3551
language, lErr := delectLanguage(workflowFilePath)
3652
if lErr != nil {
@@ -41,9 +57,9 @@ func CompileWorkflow(ctx context.Context, workflowFilePath, workflowName string)
4157
var err error
4258
switch language {
4359
case LanguageGo:
44-
workflowWasmAbsPath, err = compileGoWorkflow(ctx, workflowFilePath, workflowName)
60+
workflowWasmAbsPath, err = compileGoWorkflow(ctx, workflowFilePath, workflowName, outputDir)
4561
case LanguageTS:
46-
workflowWasmAbsPath, err = compileTSWorkflow(ctx, workflowFilePath, workflowName)
62+
workflowWasmAbsPath, err = compileTSWorkflow(ctx, workflowFilePath, workflowName, outputDir)
4763
default:
4864
return "", fmt.Errorf("unsupported workflow language: %s", language)
4965
}
@@ -76,25 +92,25 @@ func delectLanguage(workflowFilePath string) (Language, error) {
7692
}
7793
}
7894

79-
func compileTSWorkflow(ctx context.Context, workflowFilePath, workflowName string) (string, error) {
80-
workflowWasmPath := workflowName + ".wasm"
95+
func compileTSWorkflow(ctx context.Context, workflowFilePath, workflowName, outputDir string) (string, error) {
96+
workflowWasmPath := filepath.Join(outputDir, workflowName+".wasm")
8197

82-
compileCmd := exec.CommandContext(ctx, "bun", "cre-compile", workflowFilePath, filepath.Join(filepath.Dir(workflowFilePath), workflowWasmPath)) // #nosec G204 -- we control the value of the cmd so the lint/sec error is a false positive
98+
compileCmd := exec.CommandContext(ctx, "bun", "cre-compile", workflowFilePath, workflowWasmPath) // #nosec G204 -- we control the value of the cmd so the lint/sec error is a false positive
8399
if output, err := compileCmd.CombinedOutput(); err != nil {
84100
fmt.Fprint(os.Stderr, string(output))
85101
return "", errors.Wrap(err, "failed to compile workflow")
86102
}
87103

88-
workflowWasmAbsPath, workflowWasmAbsPathErr := filepath.Abs(filepath.Join(filepath.Dir(workflowFilePath), workflowWasmPath))
104+
workflowWasmAbsPath, workflowWasmAbsPathErr := filepath.Abs(workflowWasmPath)
89105
if workflowWasmAbsPathErr != nil {
90106
return "", errors.Wrap(workflowWasmAbsPathErr, "failed to get absolute path of the workflow WASM file")
91107
}
92108

93109
return workflowWasmAbsPath, nil
94110
}
95111

96-
func compileGoWorkflow(ctx context.Context, workflowFilePath, workflowName string) (string, error) {
97-
workflowWasmPath := workflowName + ".wasm"
112+
func compileGoWorkflow(ctx context.Context, workflowFilePath, workflowName, outputDir string) (string, error) {
113+
workflowWasmPath := filepath.Join(outputDir, workflowName+".wasm")
98114

99115
goModTidyCmd := exec.CommandContext(ctx, "go", "mod", "tidy")
100116
goModTidyCmd.Dir = filepath.Dir(workflowFilePath)
@@ -110,7 +126,7 @@ func compileGoWorkflow(ctx context.Context, workflowFilePath, workflowName strin
110126
return "", errors.Wrap(err, "failed to compile workflow")
111127
}
112128

113-
workflowWasmAbsPath, workflowWasmAbsPathErr := filepath.Abs(filepath.Join(filepath.Dir(workflowFilePath), workflowWasmPath))
129+
workflowWasmAbsPath, workflowWasmAbsPathErr := filepath.Abs(workflowWasmPath)
114130
if workflowWasmAbsPathErr != nil {
115131
return "", errors.Wrap(workflowWasmAbsPathErr, "failed to get absolute path of the workflow WASM file")
116132
}

system-tests/lib/infra/docker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func PrintFailedContainerLogs(logger zerolog.Logger, logLinesCount uint64) {
6262

6363
content = strings.TrimSpace(content)
6464
if len(content) > 0 {
65-
logger.Info().Str("Container", cName).Msgf("Last 100 lines of logs")
65+
logger.Info().Str("Container", cName).Msgf("Last %d lines of logs", logLinesCount)
6666
fmt.Println(text.RedText("%s\n", content))
6767
}
6868
_ = ioReader.Close() // can't do much about the error here

0 commit comments

Comments
 (0)