package envtest
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
v1 "github.com/ignite/cli/v29/ignite/config/chain/v1"
"github.com/ignite/cli/v29/ignite/pkg/availableport"
"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
"github.com/ignite/cli/v29/ignite/pkg/gocmd"
"github.com/ignite/cli/v29/ignite/pkg/goenv"
"github.com/ignite/cli/v29/ignite/pkg/xurl"
"github.com/ignite/cli/v29/ignite/templates/field"
)
const ServeTimeout = time.Minute * 15
const (
defaultConfigFileName = "config.yml"
defaultTestTimeout = 30 * time.Minute
)
type (
Hosts struct {
RPC string
P2P string
Prof string
GRPC string
GRPCWeb string
API string
Faucet string
}
App struct {
namespace string
name string
path string
configPath string
homePath string
testTimeout time.Duration
env Env
scaffolded []scaffold
}
scaffold struct {
fields field.Fields
index field.Field
response field.Fields
params field.Fields
module string
name string
typeName string
}
)
type AppOption func(*App)
func AppConfigPath(path string) AppOption {
return func(o *App) {
o.configPath = path
}
}
func AppHomePath(path string) AppOption {
return func(o *App) {
o.homePath = path
}
}
func AppTestTimeout(d time.Duration) AppOption {
return func(o *App) {
o.testTimeout = d
}
}
func (e Env) ScaffoldApp(namespace string, flags ...string) App {
root := e.TmpDir()
e.Exec("scaffold an app",
step.NewSteps(step.New(
step.Exec(
IgniteApp,
append([]string{
"scaffold",
"chain",
namespace,
}, flags...)...,
),
step.Workdir(root),
)),
)
var (
appDirName = path.Base(namespace)
appSourcePath = filepath.Join(root, appDirName)
appHomePath = e.AppHome(appDirName)
)
e.t.Cleanup(func() { os.RemoveAll(appHomePath) })
return e.App(namespace, appSourcePath, AppHomePath(appHomePath))
}
func (e Env) App(namespace, appPath string, options ...AppOption) App {
app := App{
env: e,
path: appPath,
testTimeout: defaultTestTimeout,
scaffolded: make([]scaffold, 0),
namespace: namespace,
name: path.Base(namespace),
}
for _, apply := range options {
apply(&app)
}
if app.configPath == "" {
app.configPath = filepath.Join(appPath, defaultConfigFileName)
}
return app
}
func (a *App) SourcePath() string {
return a.path
}
func (a *App) SetHomePath(homePath string) {
a.homePath = homePath
}
func (a *App) SetConfigPath(path string) {
a.configPath = path
}
func (a *App) Binary() string {
return path.Base(a.path) + "d"
}
func (a *App) Serve(msg string, options ...ExecOption) (ok bool) {
serveCommand := []string{
"chain",
"serve",
"-v",
"--quit-on-fail",
}
if a.homePath != "" {
serveCommand = append(serveCommand, "--home", a.homePath)
}
if a.configPath != "" {
serveCommand = append(serveCommand, "--config", a.configPath)
}
a.env.t.Cleanup(func() {
appBinary := path.Join(goenv.Bin(), a.Binary())
os.Remove(appBinary)
})
return a.env.Exec(msg,
step.NewSteps(step.New(
step.Exec(IgniteApp, serveCommand...),
step.Workdir(a.path),
)),
options...,
)
}
func (a *App) Simulate(numBlocks, blockSize int) {
a.env.Exec("running the simulation tests",
step.NewSteps(step.New(
step.Exec(
IgniteApp,
"chain",
"simulate",
"--numBlocks",
strconv.Itoa(numBlocks),
"--blockSize",
strconv.Itoa(blockSize),
),
step.Workdir(a.path),
)),
)
}
func (a *App) EnsureSteady() {
_, statErr := os.Stat(a.configPath)
require.False(a.env.t, os.IsNotExist(statErr), "config.yml cannot be found")
a.env.Exec("make sure app is steady",
step.NewSteps(step.New(
step.Exec(gocmd.Name(), "test", "-timeout", a.testTimeout.String(), "./..."),
step.Workdir(a.path),
)),
)
}
func (a *App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) {
port, err := availableport.Find(1)
require.NoError(a.env.t, err)
a.EditConfig(func(c *chainconfig.Config) {
c.Faucet.Port = port[0]
c.Faucet.Coins = coins
c.Faucet.CoinsMax = coinsMax
})
addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0]))
require.NoError(a.env.t, err)
return addr
}
func (a *App) RandomizeServerPorts() Hosts {
ports, err := availableport.Find(7)
require.NoError(a.env.t, err)
genAddr := func(port uint) string {
return fmt.Sprintf("127.0.0.1:%d", port)
}
hosts := Hosts{
RPC: genAddr(ports[0]),
P2P: genAddr(ports[1]),
Prof: genAddr(ports[2]),
GRPC: genAddr(ports[3]),
GRPCWeb: genAddr(ports[4]),
API: genAddr(ports[5]),
Faucet: genAddr(ports[6]),
}
a.EditConfig(func(c *chainconfig.Config) {
c.Faucet.Host = hosts.Faucet
s := v1.Servers{}
s.GRPC.Address = hosts.GRPC
s.GRPCWeb.Address = hosts.GRPCWeb
s.API.Address = hosts.API
s.P2P.Address = hosts.P2P
s.RPC.Address = hosts.RPC
s.RPC.PProfAddress = hosts.Prof
v := &c.Validators[0]
require.NoError(a.env.t, v.SetServers(s))
})
return hosts
}
func (a *App) UseRandomHomeDir() (homeDirPath string) {
dir := a.env.TmpDir()
a.EditConfig(func(c *chainconfig.Config) {
c.Validators[0].Home = dir
})
return dir
}
func (a *App) Config() chainconfig.Config {
bz, err := os.ReadFile(a.configPath)
require.NoError(a.env.t, err)
var conf chainconfig.Config
err = yaml.Unmarshal(bz, &conf)
require.NoError(a.env.t, err)
return conf
}
func (a *App) EditConfig(apply func(*chainconfig.Config)) {
conf := a.Config()
apply(&conf)
bz, err := yaml.Marshal(conf)
require.NoError(a.env.t, err)
err = os.WriteFile(a.configPath, bz, 0o600)
require.NoError(a.env.t, err)
}
func (a *App) GenerateTSClient() bool {
return a.env.Exec("generate typescript client", step.NewSteps(
step.New(
step.Exec(IgniteApp, "g", "ts-client", "--yes", "--clear-cache"),
step.Workdir(a.path),
),
))
}
func (a *App) MustServe(ctx context.Context) {
a.env.Must(a.Serve("should serve chain", ExecCtx(ctx)))
}
func (a *App) Scaffold(msg string, shouldFail bool, typeName string, args ...string) {
a.generate(msg, "scaffold", shouldFail, append([]string{typeName}, args...)...)
if !shouldFail {
a.addScaffoldCmd(typeName, args...)
}
}
func (a *App) Generate(msg string, shouldFail bool, args ...string) {
a.generate(msg, "generate", shouldFail, args...)
}
func (a *App) generate(msg, command string, shouldFail bool, args ...string) {
opts := make([]ExecOption, 0)
if shouldFail {
opts = append(opts, ExecShouldError())
}
args = append([]string{command}, args...)
a.env.Must(a.env.Exec(msg,
step.NewSteps(step.New(
step.Exec(IgniteApp, append(args, "--yes")...),
step.Workdir(a.SourcePath()),
)),
opts...,
))
}
func (a *App) addScaffoldCmd(typeName string, args ...string) {
module := ""
index := ""
response := ""
params := ""
name := typeName
if len(args) > 0 {
name = args[0]
args = args[1:]
}
filteredArgs := make([]string, 0)
for _, arg := range args {
if strings.HasPrefix(arg, "-") {
break
}
filteredArgs = append(filteredArgs, arg)
}
for i, arg := range args {
if arg == "--no-message" {
return
}
if i+1 >= len(args) {
break
}
switch arg {
case "--module":
module = args[i+1]
case "--index":
index = args[i+1]
case "--params":
params = args[i+1]
case "-r", "--response":
response = args[i+1]
}
}
argsFields, err := field.ParseFields(filteredArgs, func(string) error { return nil })
require.NoError(a.env.t, err)
s := scaffold{
fields: argsFields,
module: module,
typeName: typeName,
name: name,
}
switch typeName {
case "map":
if index == "" {
index = "index:string"
}
indexFields, err := field.ParseFields(strings.Split(index, ","), func(string) error { return nil })
require.NoError(a.env.t, err)
require.Len(a.env.t, indexFields, 1)
s.index = indexFields[0]
case "query", "message":
if response == "" {
break
}
responseFields, err := field.ParseFields(strings.Split(response, ","), func(string) error { return nil })
require.NoError(a.env.t, err)
require.Greater(a.env.t, len(responseFields), 0)
s.response = responseFields
case "module":
s.module = name
if params == "" {
break
}
paramsFields, err := field.ParseFields(strings.Split(params, ","), func(string) error { return nil })
require.NoError(a.env.t, err)
require.Greater(a.env.t, len(paramsFields), 0)
s.params = paramsFields
case "params":
s.params = argsFields
}
a.scaffolded = append(a.scaffolded, s)
}
func (a *App) WaitChainUp(ctx context.Context, chainAPI string) {
env := a.env
stepsCheckChains := step.NewSteps(
step.New(
step.Exec(
a.Binary(),
"config",
"output", "json",
),
step.PreExec(func() error {
return env.IsAppServed(ctx, chainAPI)
}),
),
)
env.Exec(fmt.Sprintf("waiting the chain (%s) is up", chainAPI), stepsCheckChains, ExecRetry())
}