-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add first version of evm utils * Remove unused context util * Add WSServer tests * Add NewLegacyTransaction test * Update NewTestChainScopedConfig to apply correct defaults * Move testutils * Fix lint * Add changeset
- Loading branch information
Showing
6 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |