ethergo/submitter/suite_test.go
package submitter_test
import (
"context"
"database/sql"
"fmt"
"math/big"
"os"
"sync"
"testing"
"time"
"github.com/synapsecns/sanguine/ethergo/examples/contracttests"
"github.com/Flaque/filet"
"github.com/brianvoe/gofakeit/v6"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/params"
"github.com/ipfs/go-log"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/core/config"
common_base "github.com/synapsecns/sanguine/core/dbcommon"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/core/metrics/localmetrics"
"github.com/synapsecns/sanguine/core/processlog"
"github.com/synapsecns/sanguine/ethergo/backends/anvil"
"github.com/synapsecns/sanguine/ethergo/backends/simulated"
"github.com/synapsecns/sanguine/ethergo/client"
"github.com/synapsecns/sanguine/ethergo/manager"
"github.com/synapsecns/sanguine/ethergo/mocks"
"github.com/synapsecns/sanguine/ethergo/signer/nonce"
"github.com/synapsecns/sanguine/ethergo/signer/signer"
"github.com/synapsecns/sanguine/ethergo/signer/signer/localsigner"
"github.com/synapsecns/sanguine/ethergo/submitter"
"github.com/synapsecns/sanguine/ethergo/submitter/db"
"github.com/synapsecns/sanguine/ethergo/submitter/db/txdb"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"github.com/stretchr/testify/suite"
"github.com/synapsecns/sanguine/core/testsuite"
"github.com/synapsecns/sanguine/ethergo/backends"
)
var buildInfo = config.NewBuildInfo(config.DefaultVersion, config.DefaultCommit, "submitter", config.DefaultDate)
var fundAmount = new(big.Int).Mul(new(big.Int).SetUint64(uint64(params.Ether)), big.NewInt(10))
// SubmitterSuite is used to test individual contract deployments to make sure other tests don't break.
type SubmitterSuite struct {
*testsuite.TestSuite
// testBackend is the test backend
testBackends []backends.SimulatedTestBackend
// metrics is the metrics handler
metrics metrics.Handler
// signer is the signer to use for the test
signer signer.Signer
// store is the store to use for the test
store db.Service
// registry is the registry to use for the test
deployer *manager.DeployerManager
// localAccount is the local account to use for the test
localAccount *keystore.Key
}
// GetClient returns a client for the given chain id.
func (s *SubmitterSuite) GetClient(ctx context.Context, chainID *big.Int) (client.EVM, error) {
for _, backend := range s.testBackends {
if backend.GetBigChainID().Cmp(chainID) == 0 {
//nolint: wrapcheck
return client.DialBackend(ctx, backend.RPCAddress(), s.metrics)
}
}
return nil, fmt.Errorf("could not find client for chain id %v", chainID)
}
// SetupSuite sets up 3 backends and metrics.
func (s *SubmitterSuite) SetupSuite() {
s.TestSuite.SetupSuite()
testChainIDs := []uint64{1, 3, 4}
s.testBackends = make([]backends.SimulatedTestBackend, len(testChainIDs))
s.deployer = manager.NewDeployerManager(s.T(), contracttests.NewCounterDeployer)
var wg sync.WaitGroup
// wait for all the backends to be created, add 1 to the wait group for the metrics
wg.Add(len(testChainIDs) + 1)
logDir := filet.TmpDir(s.T(), "")
// create the jaeger instance
go func() {
defer wg.Done()
var err error
// don't use metrics on ci for integration tests
isCI := core.GetEnvBool("CI", false)
useMetrics := !isCI
metricsHandler := metrics.Null
if useMetrics {
localmetrics.SetupTestJaeger(s.GetSuiteContext(), s.T())
metricsHandler = metrics.Jaeger
}
s.metrics, err = metrics.NewByType(s.GetSuiteContext(), buildInfo, metricsHandler)
s.Require().NoError(err)
}()
// create the backends
for i, chainID := range testChainIDs {
go func(index int, chainID uint64) {
defer wg.Done()
options := anvil.NewAnvilOptionBuilder()
options.SetChainID(chainID)
// make sure all the docker containers log to the same directory
options.SetProcessLogOptions(processlog.WithLogFileName(fmt.Sprintf("chain-%d.log", chainID)), processlog.WithLogDir(logDir))
var err error
s.testBackends[index], err = anvil.NewAnvilBackend(s.GetSuiteContext(), s.T(), options)
s.Require().NoError(err)
s.deployer.Get(s.GetSuiteContext(), s.testBackends[index], contracttests.CounterType)
}(i, chainID)
}
wg.Wait()
}
// SetupTest sets up the signer and funds the account with 10 eth on each backend.
func (s *SubmitterSuite) SetupTest() {
s.TestSuite.SetupTest()
s.localAccount = mocks.MockAccount(s.T())
// create the local signer
s.signer = localsigner.NewSigner(s.localAccount.PrivateKey)
var wg sync.WaitGroup
wg.Add(len(s.testBackends) + 1)
// setup the db
go func() {
defer wg.Done()
var err error
s.store, err = NewSqliteStore(s.GetTestContext(), filet.TmpDir(s.T(), ""), s.metrics)
s.Require().NoError(err)
}()
// fund the account on each chain
for i := range s.testBackends {
go func(index int) {
defer wg.Done()
s.testBackends[index].FundAccount(s.GetTestContext(), s.signer.Address(), *fundAmount)
}(i)
}
wg.Wait()
}
var _ submitter.ClientFetcher = &SubmitterSuite{}
// NewSubmitterSuite creates a end-to-end test suite.
func NewSubmitterSuite(tb testing.TB) *SubmitterSuite {
tb.Helper()
return &SubmitterSuite{
TestSuite: testsuite.NewTestSuite(tb),
}
}
func TestSubmitterSuite(t *testing.T) {
suite.Run(t, NewSubmitterSuite(t))
}
// TXSubmitterDBSuite is used to test db queries across mysql and sqlite
// this is ran here rather than in the db package to avoid having to export the sqlite
// setup query to avoid confusion about how to use the library.
type TXSubmitterDBSuite struct {
*testsuite.TestSuite
dbs []db.Service
metrics metrics.Handler
testBackends []backends.SimulatedTestBackend
managers map[uint]nonce.Manager
mockAccounts []*keystore.Key
}
func NewTXSubmitterDBSuite(tb testing.TB) *TXSubmitterDBSuite {
tb.Helper()
return &TXSubmitterDBSuite{
TestSuite: testsuite.NewTestSuite(tb),
dbs: []db.Service{},
}
}
func TestTXSubmitterDBSuite(t *testing.T) {
suite.Run(t, NewTXSubmitterDBSuite(t))
}
func (t *TXSubmitterDBSuite) SetupSuite() {
t.TestSuite.SetupSuite()
// don't use metrics on ci for integration tests
isCI := core.GetEnvBool("CI", false)
useMetrics := !isCI
metricsHandler := metrics.Null
if useMetrics {
localmetrics.SetupTestJaeger(t.GetSuiteContext(), t.T())
metricsHandler = metrics.Jaeger
}
var err error
t.metrics, err = metrics.NewByType(t.GetSuiteContext(), buildInfo, metricsHandler)
t.Require().NoError(err)
}
func (t *TXSubmitterDBSuite) SetupTest() {
t.TestSuite.SetupTest()
t.dbs = []db.Service{}
t.testBackends = []backends.SimulatedTestBackend{}
t.setupMysqlDB()
sqliteStore, err := NewSqliteStore(t.GetTestContext(), filet.TmpDir(t.T(), ""), t.metrics)
t.Require().NoError(err)
t.dbs = append(t.dbs, sqliteStore)
// setup a signer for each chain
t.managers = make(map[uint]nonce.Manager)
// fund the accounts on each chain
const mockAccounts = 5
t.mockAccounts = make([]*keystore.Key, mockAccounts)
for i := 0; i < mockAccounts; i++ {
t.mockAccounts[i] = mocks.MockAccount(t.T())
}
chainIDs := []uint{1, 3, 4}
var backendMux sync.Mutex
var wg sync.WaitGroup
wg.Add(len(chainIDs))
for _, chainID := range chainIDs {
go func(chainID uint) {
defer wg.Done()
backend := simulated.NewSimulatedBackendWithChainID(t.GetTestContext(), t.T(), big.NewInt(int64(chainID)))
backendMux.Lock()
t.testBackends = append(t.testBackends, backend)
t.managers[chainID] = nonce.NewNonceManager(t.GetTestContext(), backend, backend.GetBigChainID())
backendMux.Unlock()
for _, account := range t.mockAccounts {
backend.FundAccount(t.GetTestContext(), account.Address, *fundAmount)
}
}(chainID)
}
wg.Wait()
}
func (t *TXSubmitterDBSuite) RunOnAllDBs(testFunc func(db db.Service)) {
wg := sync.WaitGroup{}
for _, testDB := range t.dbs {
wg.Add(1)
// capture the value
go func(testDB db.Service) {
defer wg.Done()
testFunc(testDB)
}(testDB)
}
wg.Wait()
}
func (t *TXSubmitterDBSuite) setupMysqlDB() {
// skip if mysql test disabled, this really only needs to be run in ci
// skip if mysql test disabled
if os.Getenv(common_base.EnableMysqlTestVar) == "" {
return
}
// sets up the conn string to the default database
connString := t.connString(os.Getenv(common_base.MysqlDatabaseVar))
// sets up the myqsl db
testDB, err := sql.Open("mysql", connString)
t.Require().NoError(err)
// close the db once the connection is don
defer func() {
t.Require().NoError(testDB.Close())
}()
mysqlStore, err := NewMysqlStore(t.GetTestContext(), connString, t.metrics)
t.Require().NoError(err)
t.dbs = append(t.dbs, mysqlStore)
}
// connString gets the mysql connection string.
func (t *TXSubmitterDBSuite) connString(dbname string) string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", core.GetEnv("MYSQL_USER", "root"), os.Getenv("MYSQL_PASSWORD"), core.GetEnv("MYSQL_HOST", "127.0.0.1"), core.GetEnvInt("MYSQL_PORT", 3306), dbname)
}
// Store wraps the store. Since tx submitter is a library and not a standalone service, we simulate db creation here.
// and then proceed as we would with any other db test.
type Store struct {
*txdb.Store
}
// NewMysqlStore creates a new mysql data store. It emulates the way another caller would create a store.
func NewMysqlStore(parentCtx context.Context, dbURL string, handler metrics.Handler) (_ *Store, err error) {
logger := log.Logger("mysql-store")
ctx, span := handler.Tracer().Start(parentCtx, "start-mysql")
defer func() {
metrics.EndSpanWithErr(span, err)
}()
namingStrategy := schema.NamingStrategy{
TablePrefix: fmt.Sprintf("test%d_%d_", gofakeit.Int64(), time.Now().Unix()),
}
gdb, err := gorm.Open(mysql.Open(dbURL), &gorm.Config{
Logger: common_base.GetGormLogger(logger),
FullSaveAssociations: true,
NamingStrategy: namingStrategy,
NowFunc: time.Now,
SkipDefaultTransaction: true,
})
if err != nil {
return nil, fmt.Errorf("could not create mysql connection: %w", err)
}
sqlDB, err := gdb.DB()
if err != nil {
return nil, fmt.Errorf("could not get sql db: %w", err)
}
// fixes a timeout issue https://stackoverflow.com/a/42146536
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)
handler.AddGormCallbacks(gdb)
// migrate in a transaction since we skip this by default
err = gdb.Transaction(func(tx *gorm.DB) error {
//nolint: wrapcheck
return gdb.WithContext(ctx).AutoMigrate(txdb.GetAllModels()...)
})
if err != nil {
return nil, fmt.Errorf("could not migrate on mysql: %w", err)
}
return &Store{txdb.NewTXStore(gdb, handler)}, nil
}
// NewSqliteStore creates a new sqlite data store.
func NewSqliteStore(parentCtx context.Context, dbPath string, handler metrics.Handler) (_ *Store, err error) {
logger := log.Logger("sqlite-store")
logger.Debugf("creating sqlite store at %s", dbPath)
ctx, span := handler.Tracer().Start(parentCtx, "start-sqlite")
defer func() {
metrics.EndSpanWithErr(span, err)
}()
// create the directory to the store if it doesn't exist
err = os.MkdirAll(dbPath, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("could not create sqlite store")
}
logger.Warnf("submitter database is at %s/synapse.db", dbPath)
namingStrategy := schema.NamingStrategy{
TablePrefix: fmt.Sprintf("test%d_%d_", gofakeit.Int64(), time.Now().Unix()),
}
gdb, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s/%s", dbPath, "synapse.db")), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: common_base.GetGormLogger(logger),
FullSaveAssociations: true,
SkipDefaultTransaction: true,
NamingStrategy: namingStrategy,
})
if err != nil {
return nil, fmt.Errorf("could not connect to db %s: %w", dbPath, err)
}
handler.AddGormCallbacks(gdb)
err = gdb.WithContext(ctx).AutoMigrate(txdb.GetAllModels()...)
if err != nil {
return nil, fmt.Errorf("could not migrate models: %w", err)
}
return &Store{txdb.NewTXStore(gdb, handler)}, nil
}