Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer testutils #12968

Merged
merged 9 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-rice-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

Moved test functions under evm package to support evm extraction #internal
198 changes: 198 additions & 0 deletions core/chains/evm/testutils/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package testutils

import (
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"

"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"

evmclmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks"
)

func NewEthClientMock(t *testing.T) *evmclmocks.Client {
return evmclmocks.NewClient(t)
}

func NewEthClientMockWithDefaultChain(t *testing.T) *evmclmocks.Client {
c := NewEthClientMock(t)
c.On("ConfiguredChainID").Return(FixtureChainID).Maybe()
//c.On("IsL2").Return(false).Maybe()
return c
}

// JSONRPCHandler is called with the method and request param(s).
// respResult will be sent immediately. notifyResult is optional, and sent after a short delay.
type JSONRPCHandler func(reqMethod string, reqParams gjson.Result) JSONRPCResponse

type JSONRPCResponse struct {
Result, Notify string // raw JSON (i.e. quoted strings etc.)

Error struct {
Code int
Message string
}
}

type testWSServer struct {
t *testing.T
s *httptest.Server
mu sync.RWMutex
wsconns []*websocket.Conn
wg sync.WaitGroup
}

// NewWSServer starts a websocket server which invokes callback for each message received.
// If chainID is set, then eth_chainId calls will be automatically handled.
func NewWSServer(t *testing.T, chainID *big.Int, callback JSONRPCHandler) (ts *testWSServer) {
ts = new(testWSServer)
ts.t = t
ts.wsconns = make([]*websocket.Conn, 0)
handler := ts.newWSHandler(chainID, callback)
ts.s = httptest.NewServer(handler)
t.Cleanup(ts.Close)
return
}

func (ts *testWSServer) Close() {
if func() bool {
ts.mu.Lock()
defer ts.mu.Unlock()
if ts.wsconns == nil {
ts.t.Log("Test WS server already closed")
return false
}
ts.s.CloseClientConnections()
ts.s.Close()
for _, ws := range ts.wsconns {
ws.Close()
}
ts.wsconns = nil // nil indicates server closed
return true
}() {
ts.wg.Wait()
}
}

func (ts *testWSServer) WSURL() *url.URL {
return WSServerURL(ts.t, ts.s)
}

// WSServerURL returns a ws:// url for the server
func WSServerURL(t *testing.T, s *httptest.Server) *url.URL {
u, err := url.Parse(s.URL)
require.NoError(t, err, "Failed to parse url")
u.Scheme = "ws"
return u
}

func (ts *testWSServer) MustWriteBinaryMessageSync(t *testing.T, msg string) {
ts.mu.Lock()
defer ts.mu.Unlock()
conns := ts.wsconns
if len(conns) != 1 {
t.Fatalf("expected 1 conn, got %d", len(conns))
}
conn := conns[0]
err := conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
require.NoError(t, err)
}

func (ts *testWSServer) newWSHandler(chainID *big.Int, callback JSONRPCHandler) (handler http.HandlerFunc) {
if callback == nil {
callback = func(method string, params gjson.Result) (resp JSONRPCResponse) { return }
}
t := ts.t
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
return func(w http.ResponseWriter, r *http.Request) {
ts.mu.Lock()
if ts.wsconns == nil { // closed
ts.mu.Unlock()
return
}
ts.wg.Add(1)
defer ts.wg.Done()
conn, err := upgrader.Upgrade(w, r, nil)
if !assert.NoError(t, err, "Failed to upgrade WS connection") {
ts.mu.Unlock()
return
}
defer conn.Close()
ts.wsconns = append(ts.wsconns, conn)
ts.mu.Unlock()

for {
_, data, err := conn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseAbnormalClosure) {
ts.t.Log("Websocket closing")
return
}
ts.t.Logf("Failed to read message: %v", err)
return
}
ts.t.Log("Received message", string(data))
req := gjson.ParseBytes(data)
if !req.IsObject() {
ts.t.Logf("Request must be object: %v", req.Type)
return
}
if e := req.Get("error"); e.Exists() {
ts.t.Logf("Received jsonrpc error: %v", e)
continue
}
m := req.Get("method")
if m.Type != gjson.String {
ts.t.Logf("Method must be string: %v", m.Type)
return
}

var resp JSONRPCResponse
if chainID != nil && m.String() == "eth_chainId" {
resp.Result = `"0x` + chainID.Text(16) + `"`
} else if m.String() == "eth_syncing" {
resp.Result = "false"
} else {
resp = callback(m.String(), req.Get("params"))
}
id := req.Get("id")
var msg string
if resp.Error.Message != "" {
msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"error":{"code":%d,"message":"%s"}}`, id, resp.Error.Code, resp.Error.Message)
} else {
msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"result":%s}`, id, resp.Result)
}
ts.t.Logf("Sending message: %v", msg)
ts.mu.Lock()
err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
ts.mu.Unlock()
if err != nil {
ts.t.Logf("Failed to write message: %v", err)
return
}

if resp.Notify != "" {
time.Sleep(100 * time.Millisecond)
msg := fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":%s}}`, resp.Notify)
ts.t.Log("Sending message", msg)
ts.mu.Lock()
err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
ts.mu.Unlock()
if err != nil {
ts.t.Logf("Failed to write message: %v", err)
return
}
}
}
}
}
29 changes: 29 additions & 0 deletions core/chains/evm/testutils/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package testutils

import (
"testing"

"github.com/smartcontractkit/chainlink-common/pkg/logger"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big"
)

func NewTestChainScopedConfig(t testing.TB, overrideFn func(c *toml.EVMConfig)) config.ChainScopedConfig {
var chainID = (*big.Big)(FixtureChainID)
evmCfg := &toml.EVMConfig{
ChainID: chainID,
Chain: toml.Defaults(chainID),
}

if overrideFn != nil {
// We need to get the chainID from the override function first to load the correct chain defaults.
// Then we apply the override values on top
overrideFn(evmCfg)
evmCfg.Chain = toml.Defaults(evmCfg.ChainID)
overrideFn(evmCfg)
}

return config.NewTOMLChainScopedConfig(evmCfg, logger.Test(t))
}
22 changes: 22 additions & 0 deletions core/chains/evm/testutils/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package testutils

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
)

func TestNewTestChainScopedConfigOverride(t *testing.T) {
c := NewTestChainScopedConfig(t, func(c *toml.EVMConfig) {
finalityDepth := uint32(100)
c.FinalityDepth = &finalityDepth
})

// Overrides values
assert.Equal(t, uint32(100), c.EVM().FinalityDepth())
// fallback.toml values
assert.Equal(t, false, c.EVM().GasEstimator().EIP1559DynamicFees())

}
76 changes: 76 additions & 0 deletions core/chains/evm/testutils/evmtypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package testutils

import (
"crypto/rand"
"fmt"
"math"
"math/big"
mrand "math/rand"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types"
evmutils "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils"
ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big"
)

// FixtureChainID matches the chain always added by fixtures.sql
// It is set to 0 since no real chain ever has this ID and allows a virtual
// "test" chain ID to be used without clashes
var FixtureChainID = big.NewInt(0)

// SimulatedChainID is the chain ID for the go-ethereum simulated backend
var SimulatedChainID = big.NewInt(1337)

// NewRandomEVMChainID returns a suitable random chain ID that will not conflict
// with fixtures
func NewRandomEVMChainID() *big.Int {
id := mrand.Int63n(math.MaxInt32) + 10000
return big.NewInt(id)
}

// NewAddress return a random new address
func NewAddress() common.Address {
return common.BytesToAddress(randomBytes(20))
}

func randomBytes(n int) []byte {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return b
}

// Head given the value convert it into an Head
func Head(val interface{}) *evmtypes.Head {
var h evmtypes.Head
time := uint64(0)
switch t := val.(type) {
case int:
h = evmtypes.NewHead(big.NewInt(int64(t)), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID))
case uint64:
h = evmtypes.NewHead(big.NewInt(int64(t)), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID))
case int64:
h = evmtypes.NewHead(big.NewInt(t), evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID))
case *big.Int:
h = evmtypes.NewHead(t, evmutils.NewHash(), evmutils.NewHash(), time, ubig.New(FixtureChainID))
default:
panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val))
}
return &h
}

func NewLegacyTransaction(nonce uint64, to common.Address, value *big.Int, gasLimit uint32, gasPrice *big.Int, data []byte) *types.Transaction {
tx := types.LegacyTx{
Nonce: nonce,
To: &to,
Value: value,
Gas: uint64(gasLimit),
GasPrice: gasPrice,
Data: data,
}
return types.NewTx(&tx)
}
41 changes: 41 additions & 0 deletions core/chains/evm/testutils/timeout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package testutils

import (
"testing"
"time"
)

type Awaiter chan struct{}

func NewAwaiter() Awaiter { return make(Awaiter) }

func (a Awaiter) ItHappened() { close(a) }

func (a Awaiter) AssertHappened(t *testing.T, expected bool) {
t.Helper()
select {
case <-a:
if !expected {
t.Fatal("It happened")
}
default:
if expected {
t.Fatal("It didn't happen")
}
}
}

func (a Awaiter) AwaitOrFail(t testing.TB, durationParams ...time.Duration) {
t.Helper()

duration := 10 * time.Second
if len(durationParams) > 0 {
duration = durationParams[0]
}

select {
case <-a:
case <-time.After(duration):
t.Fatal("Timed out waiting for Awaiter to get ItHappened")
}
}
Loading