Skip to content

Commit

Permalink
syncing with core integration tests (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnieeG authored Nov 15, 2023
1 parent 9488826 commit b25df79
Show file tree
Hide file tree
Showing 117 changed files with 4,880 additions and 1,269 deletions.
78 changes: 78 additions & 0 deletions integration-tests/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
run:
timeout: 15m
linters:
enable:
- exhaustive
- exportloopref
- revive
- goimports
- gosec
- misspell
- rowserrcheck
- errorlint
linters-settings:
exhaustive:
default-signifies-exhaustive: true
goimports:
local-prefixes: github.com/smartcontractkit/chainlink
golint:
min-confidence: 0.999
gosec:
excludes:
- G101
govet:
# report about shadowed variables
check-shadowing: true
revive:
confidence: 0.8
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: if-return
- name: increment-decrement
# - name: var-naming // doesn't work with some generated names
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id
- name: waitgroup-by-value
- name: unconditional-recursion
- name: struct-tag
- name: string-format
- name: string-of-int
- name: range-val-address
- name: range-val-in-closure
- name: modifies-value-receiver
- name: modifies-parameter
- name: identical-branches
- name: get-return
# - name: flag-parameter // probably one we should work on doing better at in the future
# - name: early-return // probably one we should work on doing better at in the future
- name: defer
- name: constant-logical-expr
- name: confusing-naming
- name: confusing-results
- name: bool-literal-in-expr
- name: atomic
issues:
exclude-rules:
- text: "^G404: Use of weak random number generator"
linters:
- gosec
- linters:
- govet
text: "declaration of \"err\" shadows"
3 changes: 2 additions & 1 deletion integration-tests/.tool-versions
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
golang 1.21.1
golang 1.21.4
k3d 5.4.6
kubectl 1.25.5
nodejs 18.13.0
golangci-lint 1.55.2
163 changes: 163 additions & 0 deletions integration-tests/LOG_POLLER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# How to run Log Poller's tests

## Limitations
* currently they can only be run in Docker, not in Kubernetes
* when using `looped` runner it's not possible to directly control execution time
* WASP's `gun` implementation is imperfect in terms of generated load

## Configuration
Due to unfinished migration to TOML config tests use a mixed configuration approach:
* network, RPC endpoints, funding keys, etc need to be provided by env vars
* test-specific configuration can be provided by TOML file or via a `Config` struct (to which TOML is parsed anyway) additionally some of it can be overridden by env vars (for ease of use in CI)
** smoke tests use the programmatical approach
** load test uses the TOML approach

## Approximated test scenario
Different tests might have slightly modified scenarios, but generally they follow this pattern:
* start CL nodes
* setup OCR
* upload Automation Registry 2.1
* deploy UpKeep Consumers
* deploy test contracts
* register filters for test contracts
* make sure all CL nodes have filters registered
* emit test logs
* wait for log poller to finalise last block in which logs were emitted
** block number is determined either by finality tag or fixed finality depth depending on network configuration
* wait for all CL nodes to have expected log count
* compare logs that present in the EVM node with logs in CL nodes

All of the checks use fluent waits.

### Required env vars
* `CHAINLINK_IMAGE`
* `CHAINLINK_VERSION`
* `SELECTED_NETWORKS`

### Env vars required for live testnet tests
* `EVM_WS_URL` -- RPC websocket
* `EVM_HTTP_URL` -- RPC HTTP
* `EVM_KEYS` -- private keys used for funding

Since on live testnets we are using existing and canonical LINK contracts funding keys need to contain enough LINK to pay for the test. There's an automated check that fails during setup if there's not enough LINK. Approximately `9 LINK` is required for each UpKeep contract test uses to register a `LogTrigger`. Test contract emits 3 types of events and unless configured otherwise (programmatically!) all of them will be used, which means that due to Automation's limitation we need to register a separate `LogTrigger` for each event type for each contract. So if you want to test with 100 contracts, then you'd need to register 300 UpKeep contracts and thus your funding address needs to have at least 2700 LINK.

### Programmatical config
There are two load generators available:
* `looped` -- it's a simple generator that just loops over all contracts and emits events at random intervals
* `wasp` -- based on WASP load testing tool, it's more sophisticated and allows to control execution time

#### Looped config
```
cfg := logpoller.Config{
General: &logpoller.General{
Generator: logpoller.GeneratorType_Looped,
Contracts: 2, # number of test contracts to deploy
EventsPerTx: 4, # number of events to emit in a single transaction
UseFinalityTag: false, # if set to true then Log Poller will use finality tag returned by chain, when determining last finalised block (won't work on a simulated network, it requires eth2)
},
LoopedConfig: &logpoller.LoopedConfig{
ContractConfig: logpoller.ContractConfig{
ExecutionCount: 100, # number of times each contract will be called
},
FuzzConfig: logpoller.FuzzConfig{
MinEmitWaitTimeMs: 200, # minimum number of milliseconds to wait before emitting events
MaxEmitWaitTimeMs: 500, # maximum number of milliseconds to wait before emitting events
},
},
}
eventsToEmit := []abi.Event{}
for _, event := range logpoller.EmitterABI.Events { # modify that function to emit only logs you want
eventsToEmit = append(eventsToEmit, event)
}
cfg.General.EventsToEmit = eventsToEmit
```

Remember that final number of events emitted will be `Contracts * EventsPerTx * ExecutionCount * len(eventToEmit)`. And that that last number by default is equal to `3` (that's because we want to emit different event types, not just one). You can change that by overriding `EventsToEmit` field.

#### WASP config
```
cfg := logpoller.Config{
General: &logpoller.General{
Generator: logpoller.GeneratorType_Looped,
Contracts: 2,
EventsPerTx: 4,
UseFinalityTag: false,
},
Wasp: &logpoller.WaspConfig{
Load: &logpoller.Load{
RPS: 10, # requests per second
LPS: 0, # logs per second
RateLimitUnitDuration: models.MustNewDuration(5 * time.Minutes), # for how long the load should be limited (ramp-up period)
Duration: models.MustNewDuration(5 * time.Minutes), # how long to generate the load for
CallTimeout: models.MustNewDuration(5 * time.Minutes), # how long to wait for a single call to finish
},
},
}
eventsToEmit := []abi.Event{}
for _, event := range logpoller.EmitterABI.Events {
eventsToEmit = append(eventsToEmit, event)
}
cfg.General.EventsToEmit = eventsToEmit
```

Remember that you cannot specify both `RPS` and `LPS`. If you want to use `LPS` then omit `RPS` field. Also remember that depending on the events you decide to emit RPS might mean 1 request or might mean 3 requests (if you go with the default `EventsToEmit`).

For other nuances do check [gun.go][integration-tests/universal/log_poller/gun.go].

### TOML config
That config follows the same structure as programmatical config shown above.

Sample config: [config.toml](integration-tests/load/log_poller/config.toml)

Use this snippet instead of creating the `Config` struct programmatically:
```
cfg, err := lp_helpers.ReadConfig(lp_helpers.DefaultConfigFilename)
require.NoError(t, err)
```

And remember to add events you want emit:
```
eventsToEmit := []abi.Event{}
for _, event := range lp_helpers.EmitterABI.Events {
eventsToEmit = append(eventsToEmit, event)
}
cfg.General.EventsToEmit = eventsToEmit
```

### Timeouts
Various checks inside the tests have hardcoded timeouts, which might not be suitable for your execution parameters, for example if you decided to emit 1M logs, then waiting for all of them to be indexed for `1m` might not be enough. Remember to adjust them accordingly.

Sample snippet:
```
gom.Eventually(func(g gomega.Gomega) {
logCountMatches, err := clNodesHaveExpectedLogCount(startBlock, endBlock, testEnv.EVMClient.GetChainID(), totalLogsEmitted, expectedFilters, l, coreLogger, testEnv.ClCluster)
if err != nil {
l.Warn().Err(err).Msg("Error checking if CL nodes have expected log count. Retrying...")
}
g.Expect(logCountMatches).To(gomega.BeTrue(), "Not all CL nodes have expected log count")
}, "1m", "30s").Should(gomega.Succeed()) # 1m is the timeout for all nodes to have expected log count
```

## Tests
* [Load](integration-tests/load/log_poller/log_poller_test.go)
* [Smoke](integration-tests/smoke/log_poller/log_poller_test.go)

## Running tests
After setting all the environment variables you can run the test with:
```
# run in the root folder of chainlink repo
go test -v -test.timeout=2700s -run TestLogPollerReplay integration-tests/smoke/log_poller_test.go
```

Remember to adjust test timeout accordingly to match expected duration.


## Github Actions
If all of that seems too complicated use this [on-demand workflow](https://github.com/smartcontractkit/chainlink/actions/workflows/on-demand-log-poller.yml).

Execution time here is an approximation, so depending on network conditions it might be slightly longer or shorter.
8 changes: 7 additions & 1 deletion integration-tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ install_gotestfmt:
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
set -euo pipefail

lint:
golangci-lint --color=always run ./... --fix -v

build:
@go build ./... && SELECTED_NETWORKS=SIMULATED go test -run=^# ./...

# Builds the test image
# tag: the tag for the test image being built, example: tag=tate
# base_tag: the tag for the base-test-image to use, example: base_tag=latest
Expand Down Expand Up @@ -118,7 +124,7 @@ test_chaos_verbose: ## Run all smoke tests with verbose logging

# Performance
.PHONY: test_perf
test_perf: test_need_operator_assets ## Run core node performance tests.
test_perf: ## Run core node performance tests.
TEST_LOG_LEVEL="disabled" \
SELECTED_NETWORKS="SIMULATED,SIMULATED_1,SIMULATED_2" \
go test -timeout 1h -count=1 -json $(args) ./performance 2>&1 | tee /tmp/gotest.log | gotestfmt
Expand Down
33 changes: 24 additions & 9 deletions integration-tests/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@
package actions

import (
"crypto/ecdsa"
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"

"github.com/ethereum/go-ethereum/crypto"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"go.uber.org/zap/zapcore"

"github.com/smartcontractkit/chainlink-env/environment"
"github.com/smartcontractkit/chainlink-testing-framework/blockchain"
ctfClient "github.com/smartcontractkit/chainlink-testing-framework/client"
"github.com/smartcontractkit/chainlink-testing-framework/k8s/environment"
"github.com/smartcontractkit/chainlink-testing-framework/logging"
"github.com/smartcontractkit/chainlink-testing-framework/testreporters"
"github.com/smartcontractkit/chainlink-testing-framework/utils"
Expand Down Expand Up @@ -252,15 +254,14 @@ func GetMockserverInitializerDataForOTPE(
func TeardownSuite(
t *testing.T,
env *environment.Environment,
logsFolderPath string,
chainlinkNodes []*client.ChainlinkK8sClient,
optionalTestReporter testreporters.TestReporter, // Optionally pass in a test reporter to log further metrics
failingLogLevel zapcore.Level, // Examines logs after the test, and fails the test if any Chainlink logs are found at or above provided level
clients ...blockchain.EVMClient,
) error {
l := logging.GetTestLogger(t)
if err := testreporters.WriteTeardownLogs(t, env, optionalTestReporter, failingLogLevel); err != nil {
return errors.Wrap(err, "Error dumping environment logs, leaving environment running for manual retrieval")
return fmt.Errorf("Error dumping environment logs, leaving environment running for manual retrieval, err: %w", err)
}
// Delete all jobs to stop depleting the funds
err := DeleteAllJobs(chainlinkNodes)
Expand Down Expand Up @@ -328,16 +329,16 @@ func DeleteAllJobs(chainlinkNodes []*client.ChainlinkK8sClient) error {
}
jobs, _, err := node.ReadJobs()
if err != nil {
return errors.Wrap(err, "error reading jobs from chainlink node")
return fmt.Errorf("error reading jobs from chainlink node, err: %w", err)
}
for _, maps := range jobs.Data {
if _, ok := maps["id"]; !ok {
return errors.Errorf("error reading job id from chainlink node's jobs %+v", jobs.Data)
return fmt.Errorf("error reading job id from chainlink node's jobs %+v", jobs.Data)
}
id := maps["id"].(string)
_, err := node.DeleteJob(id)
if err != nil {
return errors.Wrap(err, "error deleting job from chainlink node")
return fmt.Errorf("error deleting job from chainlink node, err: %w", err)
}
}
}
Expand All @@ -348,7 +349,7 @@ func DeleteAllJobs(chainlinkNodes []*client.ChainlinkK8sClient) error {
// all from a remote, k8s style environment
func ReturnFunds(chainlinkNodes []*client.ChainlinkK8sClient, blockchainClient blockchain.EVMClient) error {
if blockchainClient == nil {
return errors.New("blockchain client is nil, unable to return funds from chainlink nodes")
return fmt.Errorf("blockchain client is nil, unable to return funds from chainlink nodes")
}
log.Info().Msg("Attempting to return Chainlink node funds to default network wallets")
if blockchainClient.NetworkSimulated() {
Expand Down Expand Up @@ -414,7 +415,7 @@ func UpgradeChainlinkNodeVersions(
nodes ...*client.ChainlinkK8sClient,
) error {
if newImage == "" && newVersion == "" {
return errors.New("unable to upgrade node version, found empty image and version, must provide either a new image or a new version")
return fmt.Errorf("unable to upgrade node version, found empty image and version, must provide either a new image or a new version")
}
for _, node := range nodes {
if err := node.UpgradeVersion(testEnvironment, newImage, newVersion); err != nil {
Expand Down Expand Up @@ -443,3 +444,17 @@ func DeployMockETHLinkFeed(cd contracts.ContractDeployer, answer *big.Int) (cont
}
return mockETHLINKFeed, err
}

// todo - move to CTF
func GenerateWallet() (common.Address, error) {
privateKey, err := crypto.GenerateKey()
if err != nil {
return common.Address{}, err
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return common.Address{}, fmt.Errorf("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
return crypto.PubkeyToAddress(*publicKeyECDSA), nil
}
7 changes: 4 additions & 3 deletions integration-tests/actions/actions_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
package actions

import (
"github.com/pkg/errors"
"fmt"

"github.com/smartcontractkit/chainlink/integration-tests/docker/test_env"
)

Expand All @@ -13,10 +14,10 @@ func UpgradeChainlinkNodeVersionsLocal(
nodes ...*test_env.ClNode,
) error {
if newImage == "" && newVersion == "" {
return errors.New("unable to upgrade node version, found empty image and version, must provide either a new image or a new version")
return fmt.Errorf("unable to upgrade node version, found empty image and version, must provide either a new image or a new version")
}
for _, node := range nodes {
if err := node.UpgradeVersion(node.NodeConfig, newImage, newVersion); err != nil {
if err := node.UpgradeVersion(newImage, newVersion); err != nil {
return err
}
}
Expand Down
Loading

0 comments on commit b25df79

Please sign in to comment.